packages/sql-faker/tests/Unit/Compiler/Bison/BisonGrammarCompilerTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Compiler\Bison;
6
7use BisonParser\Ast\Declaration\Code;
8use BisonParser\Ast\Declaration\CodeProps;
9use BisonParser\Ast\Declaration\Declaration;
10use BisonParser\Ast\Declaration\Define;
11use BisonParser\Ast\Declaration\DefineForm;
12use BisonParser\Ast\Declaration\Expect;
13use BisonParser\Ast\Declaration\Flag;
14use BisonParser\Ast\Declaration\InitialAction;
15use BisonParser\Ast\Declaration\Option;
16use BisonParser\Ast\Declaration\Param;
17use BisonParser\Ast\Declaration\ParamKind;
18use BisonParser\Ast\Declaration\Prologue;
19use BisonParser\Ast\Declaration\Start;
20use BisonParser\Ast\Declaration\Symbols\Alias;
21use BisonParser\Ast\Declaration\Symbols\Associativity;
22use BisonParser\Ast\Declaration\Symbols\PrecedenceDeclaration;
23use BisonParser\Ast\Declaration\Symbols\SymbolClass;
24use BisonParser\Ast\Declaration\Symbols\SymbolDeclaration;
25use BisonParser\Ast\Declaration\Symbols\SymbolEntry;
26use BisonParser\Ast\Declaration\UnionDeclaration;
27use BisonParser\Ast\Epilogue;
28use BisonParser\Ast\GrammarFile;
29use BisonParser\Ast\Location;
30use BisonParser\Ast\Rule\Action;
31use BisonParser\Ast\Rule\Alternative;
32use BisonParser\Ast\Rule\DprecItem;
33use BisonParser\Ast\Rule\EmptyItem;
34use BisonParser\Ast\Rule\ExpectItem;
35use BisonParser\Ast\Rule\MergeItem;
36use BisonParser\Ast\Rule\PrecItem;
37use BisonParser\Ast\Rule\Predicate;
38use BisonParser\Ast\Rule\RhsItem;
39use BisonParser\Ast\Rule\Rule;
40use BisonParser\Ast\Rule\SymbolItem;
41use BisonParser\Ast\Symbol;
42use BisonParser\Ast\SymbolKind;
43use BisonParser\Ast\Tag;
44use BisonParser\Parser;
45use BisonParser\Printer\DeclarationPrinter;
46use BisonParser\Printer\Printer;
47use BisonParser\Printer\RhsPrinter;
48use BisonParser\Printer\SymbolPrinter;
49use BisonParser\Scanner\CodeReader;
50use BisonParser\Scanner\Cursor;
51use BisonParser\Scanner\Directives;
52use BisonParser\Scanner\Escapes;
53use BisonParser\Scanner\Scanner;
54use BisonParser\Scanner\Token;
55use BisonParser\Scanner\TokenKind;
56use BisonParser\Syntax\DeclarationParser;
57use BisonParser\Syntax\GrammarParser;
58use BisonParser\Syntax\RuleParser;
59use BisonParser\Syntax\SymbolListParser;
60use BisonParser\Syntax\TokenStream;
61use BisonParser\SyntaxException;
62use PHPUnit\Framework\Attributes\CoversClass;
63use PHPUnit\Framework\Attributes\Small;
64use PHPUnit\Framework\Attributes\UsesClass;
65use PHPUnit\Framework\TestCase;
66use SqlFaker\Compiler\Bison\BisonGrammarCompiler;
67use SqlFaker\Compiler\UnknownSymbolException;
68use SqlFaker\Grammar\Model\Grammar;
69use SqlFaker\Grammar\Model\NonTerminal;
70use SqlFaker\Grammar\Model\Production;
71use SqlFaker\Grammar\Model\ProductionRule;
72use SqlFaker\Grammar\Model\Terminal;
73
74#[CoversClass(BisonGrammarCompiler::class)]
75#[UsesClass(Code::class)]
76#[UsesClass(CodeProps::class)]
77#[UsesClass(Declaration::class)]
78#[UsesClass(Define::class)]
79#[UsesClass(DefineForm::class)]
80#[UsesClass(Expect::class)]
81#[UsesClass(Flag::class)]
82#[UsesClass(InitialAction::class)]
83#[UsesClass(Option::class)]
84#[UsesClass(Param::class)]
85#[UsesClass(ParamKind::class)]
86#[UsesClass(Prologue::class)]
87#[UsesClass(Start::class)]
88#[UsesClass(Alias::class)]
89#[UsesClass(Associativity::class)]
90#[UsesClass(PrecedenceDeclaration::class)]
91#[UsesClass(SymbolClass::class)]
92#[UsesClass(SymbolDeclaration::class)]
93#[UsesClass(SymbolEntry::class)]
94#[UsesClass(UnionDeclaration::class)]
95#[UsesClass(Epilogue::class)]
96#[UsesClass(GrammarFile::class)]
97#[UsesClass(Location::class)]
98#[UsesClass(Action::class)]
99#[UsesClass(Alternative::class)]
100#[UsesClass(DprecItem::class)]
101#[UsesClass(EmptyItem::class)]
102#[UsesClass(ExpectItem::class)]
103#[UsesClass(MergeItem::class)]
104#[UsesClass(PrecItem::class)]
105#[UsesClass(Predicate::class)]
106#[UsesClass(RhsItem::class)]
107#[UsesClass(Rule::class)]
108#[UsesClass(SymbolItem::class)]
109#[UsesClass(Symbol::class)]
110#[UsesClass(SymbolKind::class)]
111#[UsesClass(Tag::class)]
112#[UsesClass(Parser::class)]
113#[UsesClass(DeclarationPrinter::class)]
114#[UsesClass(Printer::class)]
115#[UsesClass(RhsPrinter::class)]
116#[UsesClass(SymbolPrinter::class)]
117#[UsesClass(CodeReader::class)]
118#[UsesClass(Cursor::class)]
119#[UsesClass(Directives::class)]
120#[UsesClass(Escapes::class)]
121#[UsesClass(Scanner::class)]
122#[UsesClass(Token::class)]
123#[UsesClass(TokenKind::class)]
124#[UsesClass(SyntaxException::class)]
125#[UsesClass(DeclarationParser::class)]
126#[UsesClass(GrammarParser::class)]
127#[UsesClass(RuleParser::class)]
128#[UsesClass(SymbolListParser::class)]
129#[UsesClass(TokenStream::class)]
130#[UsesClass(UnknownSymbolException::class)]
131#[UsesClass(Grammar::class)]
132#[UsesClass(NonTerminal::class)]
133#[UsesClass(Production::class)]
134#[UsesClass(ProductionRule::class)]
135#[UsesClass(Terminal::class)]
136#[Small]
137final class BisonGrammarCompilerTest extends TestCase
138{
139 public function testCompile(): void
140 {
141 $grammar = (new BisonGrammarCompiler())->compile("%token NUM STR\n%start expr\n%%\nexpr: expr '+' term { \$\$ = \$1; } | term %prec NUM | \"lit\" ;\nterm: NUM ;\nexpr: STR ;\n");
142
143 self::assertSame('expr', $grammar->startSymbol);
144 self::assertSame(['expr', 'term'], array_keys($grammar->ruleMap));
145 self::assertSame(
146 [[[NonTerminal::class, 'expr'], [Terminal::class, '+'], [NonTerminal::class, 'term']], [[NonTerminal::class, 'term']], [], [[Terminal::class, 'STR']]],
147 array_map(static fn (Production $production): array => array_map(static fn (\SqlFaker\Grammar\Model\Symbol $symbol): array => [$symbol::class, $symbol->value()], $production->symbols), $grammar->ruleMap['expr']->alternatives),
148 );
149 }
150
151 public function testCompileTakesTheFirstRuleAsStartSymbolWithoutStart(): void
152 {
153 $grammar = (new BisonGrammarCompiler())->compile("%token NUM\n%%\nterm: NUM ;\nexpr: term ;\n");
154
155 self::assertSame('term', $grammar->startSymbol);
156 }
157
158 public function testCompileRejectsAFileWithoutRules(): void
159 {
160 $this->expectException(SyntaxException::class);
161 $this->expectExceptionMessage('Expected a rule but found end of file at 3:1');
162
163 (new BisonGrammarCompiler())->compile("%token NUM\n%%\n");
164 }
165
166 public function testCompileRejectsAnUndeclaredName(): void
167 {
168 $this->expectException(UnknownSymbolException::class);
169
170 (new BisonGrammarCompiler())->compile("%token NUM\n%%\nexpr: NUM PLUS ;\n");
171 }
172
173 public function testDeclaredTokens(): void
174 {
175 $file = (new Parser())->parse("%left '+' MINUS\n%token <int> NUM 258 \"number\" STR\n%type <t> expr\n%token LATE\n%%\nexpr: NUM ;\n");
176
177 self::assertSame(['NUM' => true, 'STR' => true, 'LATE' => true], (new BisonGrammarCompiler())->declaredTokens($file));
178 }
179
180 public function testSymbols(): void
181 {
182 $file = (new Parser())->parse("%token NUM\n%%\nexpr: expr '+' \"skipped\" NUM { act(); } ;\n");
183
184 $symbols = (new BisonGrammarCompiler())->symbols($file->rules()[0]->alternatives[0], ['expr' => true], ['NUM' => true]);
185
186 self::assertSame([[NonTerminal::class, 'expr'], [Terminal::class, '+'], [Terminal::class, 'NUM']], array_map(static fn (\SqlFaker\Grammar\Model\Symbol $symbol): array => [$symbol::class, $symbol->value()], $symbols));
187 }
188
189 public function testSymbolsRejectsAnUnknownName(): void
190 {
191 $file = (new Parser())->parse("%token NUM\n%%\nexpr: other ;\n");
192
193 $this->expectException(UnknownSymbolException::class);
194
195 (new BisonGrammarCompiler())->symbols($file->rules()[0]->alternatives[0], ['expr' => true], ['NUM' => true]);
196 }
197
198 public function testStartSymbol(): void
199 {
200 $compiler = new BisonGrammarCompiler();
201 $parser = new Parser();
202
203 self::assertSame('expr', $compiler->startSymbol($parser->parse("%token NUM\n%start expr\n%%\nterm: NUM ;\nexpr: term ;\n")));
204 self::assertSame('term', $compiler->startSymbol($parser->parse("%token NUM\n%%\nterm: NUM ;\nexpr: term ;\n")));
205 }
206}
207