packages/sql-parser/tests/Unit/Compiler/BisonGrammarReaderTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Compiler;
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\PrecedenceDeclaration;
22use BisonParser\Ast\Declaration\Symbols\SymbolClass;
23use BisonParser\Ast\Declaration\Symbols\SymbolDeclaration;
24use BisonParser\Ast\Declaration\Symbols\SymbolEntry;
25use BisonParser\Ast\Declaration\UnionDeclaration;
26use BisonParser\Ast\Epilogue;
27use BisonParser\Ast\GrammarFile;
28use BisonParser\Ast\Location;
29use BisonParser\Ast\Rule\Action;
30use BisonParser\Ast\Rule\Alternative;
31use BisonParser\Ast\Rule\DprecItem;
32use BisonParser\Ast\Rule\EmptyItem;
33use BisonParser\Ast\Rule\ExpectItem;
34use BisonParser\Ast\Rule\MergeItem;
35use BisonParser\Ast\Rule\PrecItem;
36use BisonParser\Ast\Rule\Predicate;
37use BisonParser\Ast\Rule\RhsItem;
38use BisonParser\Ast\Rule\SymbolItem;
39use BisonParser\Ast\Symbol;
40use BisonParser\Ast\SymbolKind;
41use BisonParser\Ast\Tag;
42use BisonParser\Parser;
43use BisonParser\Printer\DeclarationPrinter;
44use BisonParser\Printer\Printer;
45use BisonParser\Printer\RhsPrinter;
46use BisonParser\Printer\SymbolPrinter;
47use BisonParser\Scanner\CodeReader;
48use BisonParser\Scanner\Cursor;
49use BisonParser\Scanner\Directives;
50use BisonParser\Scanner\Escapes;
51use BisonParser\Scanner\Scanner;
52use BisonParser\Scanner\Token;
53use BisonParser\Scanner\TokenKind;
54use BisonParser\Syntax\DeclarationParser;
55use BisonParser\Syntax\GrammarParser;
56use BisonParser\Syntax\RuleParser;
57use BisonParser\Syntax\SymbolListParser;
58use BisonParser\Syntax\TokenStream;
59use BisonParser\SyntaxException;
60use PHPUnit\Framework\Attributes\CoversClass;
61use PHPUnit\Framework\Attributes\Small;
62use PHPUnit\Framework\Attributes\UsesClass;
63use PHPUnit\Framework\TestCase;
64use SqlParser\Compiler\BisonGrammarReader;
65use SqlParser\Grammar\Associativity;
66use SqlParser\Grammar\Grammar;
67use SqlParser\Grammar\GrammarBuilder;
68use SqlParser\Grammar\Precedence;
69use SqlParser\Grammar\PrecedencePolicy;
70use SqlParser\Grammar\Rule;
71use SqlParser\Grammar\SymbolTable;
72
73#[CoversClass(BisonGrammarReader::class)]
74#[UsesClass(Code::class)]
75#[UsesClass(CodeProps::class)]
76#[UsesClass(Declaration::class)]
77#[UsesClass(Define::class)]
78#[UsesClass(DefineForm::class)]
79#[UsesClass(Expect::class)]
80#[UsesClass(Flag::class)]
81#[UsesClass(InitialAction::class)]
82#[UsesClass(Option::class)]
83#[UsesClass(Param::class)]
84#[UsesClass(ParamKind::class)]
85#[UsesClass(Prologue::class)]
86#[UsesClass(Start::class)]
87#[UsesClass(Alias::class)]
88#[UsesClass(\BisonParser\Ast\Declaration\Symbols\Associativity::class)]
89#[UsesClass(PrecedenceDeclaration::class)]
90#[UsesClass(SymbolClass::class)]
91#[UsesClass(SymbolDeclaration::class)]
92#[UsesClass(SymbolEntry::class)]
93#[UsesClass(UnionDeclaration::class)]
94#[UsesClass(Epilogue::class)]
95#[UsesClass(GrammarFile::class)]
96#[UsesClass(Location::class)]
97#[UsesClass(Action::class)]
98#[UsesClass(Alternative::class)]
99#[UsesClass(DprecItem::class)]
100#[UsesClass(EmptyItem::class)]
101#[UsesClass(ExpectItem::class)]
102#[UsesClass(MergeItem::class)]
103#[UsesClass(PrecItem::class)]
104#[UsesClass(Predicate::class)]
105#[UsesClass(RhsItem::class)]
106#[UsesClass(\BisonParser\Ast\Rule\Rule::class)]
107#[UsesClass(SymbolItem::class)]
108#[UsesClass(Symbol::class)]
109#[UsesClass(SymbolKind::class)]
110#[UsesClass(Tag::class)]
111#[UsesClass(Parser::class)]
112#[UsesClass(DeclarationPrinter::class)]
113#[UsesClass(Printer::class)]
114#[UsesClass(RhsPrinter::class)]
115#[UsesClass(SymbolPrinter::class)]
116#[UsesClass(CodeReader::class)]
117#[UsesClass(Cursor::class)]
118#[UsesClass(Directives::class)]
119#[UsesClass(Escapes::class)]
120#[UsesClass(Scanner::class)]
121#[UsesClass(Token::class)]
122#[UsesClass(TokenKind::class)]
123#[UsesClass(SyntaxException::class)]
124#[UsesClass(DeclarationParser::class)]
125#[UsesClass(GrammarParser::class)]
126#[UsesClass(RuleParser::class)]
127#[UsesClass(SymbolListParser::class)]
128#[UsesClass(TokenStream::class)]
129#[UsesClass(Associativity::class)]
130#[UsesClass(Grammar::class)]
131#[UsesClass(GrammarBuilder::class)]
132#[UsesClass(Precedence::class)]
133#[UsesClass(PrecedencePolicy::class)]
134#[UsesClass(Rule::class)]
135#[UsesClass(SymbolTable::class)]
136#[Small]
137final class BisonGrammarReaderTest extends TestCase
138{
139 public function testRead(): void
140 {
141 $grammar = (new BisonGrammarReader())->read("%token NUM STR\n%left '+' '-'\n%right UMINUS\n%start expr\n%expect 1\n%%\nexpr: expr '+' expr | '-' expr %prec UMINUS | NUM { \$\$ = \$1; } | \"str\" ;\n");
142
143 self::assertSame('expr', $grammar->symbols->name($grammar->startSymbol()));
144 self::assertSame(1, $grammar->expectedConflicts);
145 self::assertSame(PrecedencePolicy::LastTerminal, $grammar->policy);
146 self::assertSame(['$accept', 'expr', 'expr', 'expr', 'expr'], array_map(static fn (Rule $rule): string => $grammar->symbols->name($rule->lhs), $grammar->rules));
147 self::assertSame(['-', 'expr'], array_map(static fn (int $id): string => $grammar->symbols->name($id), $grammar->rules[2]->rhs));
148 self::assertSame('UMINUS', $grammar->symbols->name($grammar->rules[2]->precedenceSymbol ?? -1));
149 self::assertSame('str', $grammar->symbols->name($grammar->rules[4]->rhs[0]));
150 self::assertNotNull($grammar->symbols->id('error'));
151 self::assertSame(Associativity::Right, $grammar->precedenceOf($grammar->symbols->id('UMINUS') ?? -1)?->associativity);
152 }
153
154 public function testReadNumbersMidRuleActionsBeforeTheirRule(): void
155 {
156 $grammar = (new BisonGrammarReader())->read("%token A B C\n%%\ns: A { one(); } B { two(); } C { last(); } | %empty ;\nt: s ;\n");
157
158 self::assertSame(['$accept', '$@1', '$@2', 's', 's', 't'], array_map(static fn (Rule $rule): string => $grammar->symbols->name($rule->lhs), $grammar->rules));
159 self::assertSame(['A', '$@1', 'B', '$@2', 'C'], array_map(static fn (int $id): string => $grammar->symbols->name($id), $grammar->rules[3]->rhs));
160 self::assertTrue($grammar->rules[1]->hidden);
161 self::assertSame([], $grammar->rules[4]->rhs);
162 }
163
164 public function testReadRejectsWhatBisonRejects(): void
165 {
166 $this->expectException(SyntaxException::class);
167
168 (new BisonGrammarReader())->read("%token A\nexpr: A;\n");
169 }
170
171 public function testDeclaration(): void
172 {
173 $file = (new Parser())->parse("%token <int> NUM 258 \"number\"\n%nonassoc EQ\n%precedence LOW\n%type <t> expr\n%start expr\n%expect-rr 2\n%expect 3\n%%\nexpr: NUM;\n");
174 $reader = new BisonGrammarReader();
175 $builder = new GrammarBuilder();
176 $builder->policy(PrecedencePolicy::LastTerminal);
177
178 $declarations = $file->allDeclarations();
179
180 $reader->declaration($declarations[0], $builder);
181 $reader->declaration($declarations[1], $builder);
182 $reader->declaration($declarations[2], $builder);
183 $reader->declaration($declarations[3], $builder);
184 $reader->declaration($declarations[4], $builder);
185 $reader->declaration($declarations[5], $builder);
186 $reader->declaration($declarations[6], $builder);
187 $builder->rule('other', ['NUM'], null);
188 $builder->rule('expr', ['NUM'], null);
189 $grammar = $builder->build();
190
191 self::assertTrue($builder->isTerminal('NUM'));
192 self::assertTrue($builder->isTerminal('EQ'));
193 self::assertSame(3, $grammar->expectedConflicts);
194 self::assertSame('expr', $grammar->symbols->name($grammar->startSymbol()));
195 self::assertSame(Associativity::Precedence, $grammar->precedenceOf($grammar->symbols->id('LOW') ?? -1)?->associativity);
196 }
197
198 public function testAssociativity(): void
199 {
200 $reader = new BisonGrammarReader();
201
202 self::assertSame(Associativity::Left, $reader->associativity(\BisonParser\Ast\Declaration\Symbols\Associativity::Left));
203 self::assertSame(Associativity::Right, $reader->associativity(\BisonParser\Ast\Declaration\Symbols\Associativity::Right));
204 self::assertSame(Associativity::NonAssoc, $reader->associativity(\BisonParser\Ast\Declaration\Symbols\Associativity::NonAssoc));
205 self::assertSame(Associativity::Precedence, $reader->associativity(\BisonParser\Ast\Declaration\Symbols\Associativity::Precedence));
206 }
207
208 public function testAlternative(): void
209 {
210 $file = (new Parser())->parse("%token A\n%%\ns: A[x] <int>{ mid(); } 'b' %dprec 2 %merge <m> %prec A { end(); } ;\n");
211 $reader = new BisonGrammarReader();
212 $builder = new GrammarBuilder();
213 $builder->terminal('A');
214
215 $reader->alternative('s', $file->rules()[0]->alternatives[0], $builder);
216 $grammar = $builder->build();
217
218 self::assertSame(['A', '$@1', 'b'], array_map(static fn (int $id): string => $grammar->symbols->name($id), $grammar->rules[2]->rhs));
219 self::assertSame('A', $grammar->symbols->name($grammar->rules[2]->precedenceSymbol ?? -1));
220 self::assertTrue($builder->isTerminal('b'));
221 }
222
223 public function testContinues(): void
224 {
225 $file = (new Parser())->parse("%token A\n%%\ns: { a } A { b } %prec A { c } ;\n");
226 $alternative = $file->rules()[0]->alternatives[0];
227 $reader = new BisonGrammarReader();
228
229 self::assertTrue($reader->continues($alternative, 0));
230 self::assertTrue($reader->continues($alternative, 1));
231 self::assertTrue($reader->continues($alternative, 2));
232 self::assertFalse($reader->continues($alternative, 4));
233 }
234
235 public function testContinuesLooksOnlyAtWhatFollowsTheItem(): void
236 {
237 $reader = new BisonGrammarReader();
238 $parser = new Parser();
239 $symbol = $parser->parse("%token A\n%%\ns: { a } A ;\n")->rules()[0]->alternatives[0];
240 $action = $parser->parse("%token A\n%%\ns: { a } { b } ;\n")->rules()[0]->alternatives[0];
241 $predicate = $parser->parse("%token A\n%%\ns: { a } %?{ p } ;\n")->rules()[0]->alternatives[0];
242 $modifiers = $parser->parse("%token A\n%%\ns: { a } %prec A %dprec 1 ;\n")->rules()[0]->alternatives[0];
243
244 self::assertTrue($reader->continues($symbol, 0));
245 self::assertTrue($reader->continues($action, 0));
246 self::assertTrue($reader->continues($predicate, 0));
247 self::assertFalse($reader->continues($modifiers, 0));
248 self::assertFalse($reader->continues($symbol, 1));
249 }
250
251 public function testReadNumbersMidRuleActionsFromOneForEveryFile(): void
252 {
253 $reader = new BisonGrammarReader();
254 $source = "%token A B\n%%\ns: A { act(); } B ;\n";
255
256 $reader->read($source);
257 $grammar = $reader->read($source);
258
259 self::assertSame('$@1', $grammar->symbols->name($grammar->rules[1]->lhs));
260 }
261
262 public function testMidRule(): void
263 {
264 $reader = new BisonGrammarReader();
265 $builder = new GrammarBuilder();
266
267 $first = $reader->midRule($builder);
268 $second = $reader->midRule($builder);
269 $builder->rule('s', [$first, $second], null);
270 $grammar = $builder->build();
271
272 self::assertSame(['$@1', '$@2'], [$first, $second]);
273 self::assertTrue($grammar->rules[1]->hidden);
274 self::assertSame([], $grammar->rules[1]->rhs);
275 }
276}
277