packages/bison-parser/tests/Unit/Syntax/DeclarationParserTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Syntax;
6
7use BisonParser\Ast\Declaration\Code;
8use BisonParser\Ast\Declaration\CodeProps;
9use BisonParser\Ast\Declaration\Define;
10use BisonParser\Ast\Declaration\DefineForm;
11use BisonParser\Ast\Declaration\Expect;
12use BisonParser\Ast\Declaration\Flag;
13use BisonParser\Ast\Declaration\InitialAction;
14use BisonParser\Ast\Declaration\Option;
15use BisonParser\Ast\Declaration\Param;
16use BisonParser\Ast\Declaration\ParamKind;
17use BisonParser\Ast\Declaration\Prologue;
18use BisonParser\Ast\Declaration\Start;
19use BisonParser\Ast\Declaration\Symbols\Alias;
20use BisonParser\Ast\Declaration\Symbols\Associativity;
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\Line;
27use BisonParser\Ast\Location;
28use BisonParser\Ast\Symbol;
29use BisonParser\Ast\SymbolKind;
30use BisonParser\Ast\Tag;
31use BisonParser\Scanner\CodeReader;
32use BisonParser\Scanner\Cursor;
33use BisonParser\Scanner\Directives;
34use BisonParser\Scanner\Escapes;
35use BisonParser\Scanner\Scanner;
36use BisonParser\Scanner\Token;
37use BisonParser\Scanner\TokenKind;
38use BisonParser\Syntax\DeclarationParser;
39use BisonParser\Syntax\SymbolListParser;
40use BisonParser\Syntax\TokenStream;
41use BisonParser\SyntaxException;
42use PHPUnit\Framework\Attributes\CoversClass;
43use PHPUnit\Framework\Attributes\Small;
44use PHPUnit\Framework\Attributes\UsesClass;
45use PHPUnit\Framework\TestCase;
46
47#[CoversClass(DeclarationParser::class)]
48#[UsesClass(Alias::class)]
49#[UsesClass(Associativity::class)]
50#[UsesClass(Code::class)]
51#[UsesClass(CodeProps::class)]
52#[UsesClass(CodeReader::class)]
53#[UsesClass(Cursor::class)]
54#[UsesClass(Define::class)]
55#[UsesClass(DefineForm::class)]
56#[UsesClass(Directives::class)]
57#[UsesClass(Escapes::class)]
58#[UsesClass(Expect::class)]
59#[UsesClass(Flag::class)]
60#[UsesClass(InitialAction::class)]
61#[UsesClass(Line::class)]
62#[UsesClass(Location::class)]
63#[UsesClass(Option::class)]
64#[UsesClass(Param::class)]
65#[UsesClass(ParamKind::class)]
66#[UsesClass(PrecedenceDeclaration::class)]
67#[UsesClass(Prologue::class)]
68#[UsesClass(Scanner::class)]
69#[UsesClass(Start::class)]
70#[UsesClass(Symbol::class)]
71#[UsesClass(SymbolClass::class)]
72#[UsesClass(SymbolDeclaration::class)]
73#[UsesClass(SymbolEntry::class)]
74#[UsesClass(SymbolKind::class)]
75#[UsesClass(SymbolListParser::class)]
76#[UsesClass(SyntaxException::class)]
77#[UsesClass(Tag::class)]
78#[UsesClass(Token::class)]
79#[UsesClass(TokenKind::class)]
80#[UsesClass(TokenStream::class)]
81#[UsesClass(UnionDeclaration::class)]
82#[Small]
83final class DeclarationParserTest extends TestCase
84{
85 public function testStarts(): void
86 {
87 $parser = new DeclarationParser();
88
89 self::assertTrue($parser->starts(new Token(TokenKind::Prologue, '', new Location(1, 1))));
90 self::assertTrue($parser->starts(new Token(TokenKind::Directive, 'token', new Location(1, 1), '%token')));
91 self::assertFalse($parser->starts(new Token(TokenKind::Directive, 'prec', new Location(1, 1), '%prec')));
92 self::assertFalse($parser->starts(new Token(TokenKind::Directive, 'empty', new Location(1, 1), '%empty')));
93 self::assertFalse($parser->starts(new Token(TokenKind::IdentifierColon, 'expr', new Location(1, 1))));
94 }
95
96 public function testParse(): void
97 {
98 $parser = new DeclarationParser();
99 $prologue = $parser->parse(new TokenStream((new Scanner())->scan('%{ int x; %}')));
100 $flag = $parser->parse(new TokenStream((new Scanner())->scan('%debug')));
101
102 self::assertInstanceOf(Prologue::class, $prologue);
103 self::assertSame(' int x; ', $prologue->code);
104 self::assertInstanceOf(Flag::class, $flag);
105 }
106
107 public function testParseReadsALineDirective(): void
108 {
109 $line = (new DeclarationParser())->parse(new TokenStream((new Scanner())->scan("#line 7 \"x.y\"\n")));
110
111 self::assertInstanceOf(Line::class, $line);
112 self::assertSame([7, 'x.y', '1:1'], [$line->line, $line->file, (string) $line->location()]);
113 }
114
115 public function testLine(): void
116 {
117 $parser = new DeclarationParser();
118 $named = $parser->line(new Token(TokenKind::Line, '#line 12 "dir/file.y"', new Location(4, 1)));
119 $bare = $parser->line(new Token(TokenKind::Line, '#line 3', new Location(5, 1)));
120
121 self::assertSame([12, 'dir/file.y', '4:1'], [$named->line, $named->file, (string) $named->location()]);
122 self::assertSame([3, null], [$bare->line, $bare->file]);
123 }
124
125 public function testParseRejectsANonDeclaration(): void
126 {
127 $this->expectException(SyntaxException::class);
128 $this->expectExceptionMessage("Expected a declaration but found 'expr' at 1:1");
129
130 (new DeclarationParser())->parse(new TokenStream((new Scanner())->scan('expr: ;')));
131 }
132
133 public function testDirective(): void
134 {
135 $parser = new DeclarationParser();
136 $stream = new TokenStream((new Scanner())->scan(implode("\n", [
137 '%pure_parser %defines %require "3.8" %expect-rr 2 %initial-action { @$ = 1; } %lex-param {int a} {int b}',
138 '%union YYSTYPE { int i; } %start program \'x\' %printer { p($$); } <*> NUM %code requires { #include "x.h" }',
139 '%term <int> NUM 258 "number" %type <t> expr %binary \'+\' "-"',
140 ])));
141 $flag = $parser->directive($stream->next(), $stream);
142 $header = $parser->directive($stream->next(), $stream);
143 $require = $parser->directive($stream->next(), $stream);
144 $expect = $parser->directive($stream->next(), $stream);
145 $initial = $parser->directive($stream->next(), $stream);
146 $param = $parser->directive($stream->next(), $stream);
147 $union = $parser->directive($stream->next(), $stream);
148 $start = $parser->directive($stream->next(), $stream);
149 $printer = $parser->directive($stream->next(), $stream);
150 $code = $parser->directive($stream->next(), $stream);
151 $token = $parser->directive($stream->next(), $stream);
152 $type = $parser->directive($stream->next(), $stream);
153 $binary = $parser->directive($stream->next(), $stream);
154
155 self::assertTrue($stream->is(TokenKind::End));
156 self::assertInstanceOf(Flag::class, $flag);
157 self::assertSame(['pure-parser', '%pure_parser'], [$flag->name, $flag->raw]);
158 self::assertInstanceOf(Option::class, $header);
159 self::assertSame(['header', null], [$header->name, $header->value]);
160 self::assertInstanceOf(Option::class, $require);
161 self::assertSame(['require', '3.8'], [$require->name, $require->value]);
162 self::assertInstanceOf(Expect::class, $expect);
163 self::assertSame([2, true], [$expect->count, $expect->reduceReduce]);
164 self::assertInstanceOf(InitialAction::class, $initial);
165 self::assertSame(' @$ = 1; ', $initial->code);
166 self::assertInstanceOf(Param::class, $param);
167 self::assertSame([ParamKind::Lex, ['int a', 'int b']], [$param->kind, $param->codes]);
168 self::assertInstanceOf(UnionDeclaration::class, $union);
169 self::assertSame(['YYSTYPE', ' int i; '], [$union->name, $union->code]);
170 self::assertInstanceOf(Start::class, $start);
171 self::assertSame(['program', 'x'], [$start->symbols[0]->value, $start->symbols[1]->value]);
172 self::assertInstanceOf(CodeProps::class, $printer);
173 self::assertSame([true, ' p($$); ', Tag::class, Symbol::class], [$printer->printer, $printer->code, $printer->targets[0]::class, $printer->targets[1]::class]);
174 self::assertInstanceOf(Code::class, $code);
175 self::assertSame(['requires', ' #include "x.h" '], [$code->qualifier, $code->code]);
176 self::assertInstanceOf(SymbolDeclaration::class, $token);
177 self::assertSame([SymbolClass::Token, 'NUM', 'int', 258, 'number'], [$token->class, $token->entries[0]->symbol->value, $token->entries[0]->tag, $token->entries[0]->number, $token->entries[0]->alias?->text]);
178 self::assertInstanceOf(SymbolDeclaration::class, $type);
179 self::assertSame([SymbolClass::Type, 'expr', 't'], [$type->class, $type->entries[0]->symbol->value, $type->entries[0]->tag]);
180 self::assertInstanceOf(PrecedenceDeclaration::class, $binary);
181 self::assertSame([Associativity::NonAssoc, '+', '-'], [$binary->associativity, $binary->entries[0]->symbol->value, $binary->entries[1]->symbol->value]);
182 }
183
184 public function testDirectiveRejectsAnOptionWithoutItsString(): void
185 {
186 $stream = new TokenStream((new Scanner())->scan('%require 3'));
187
188 $this->expectException(SyntaxException::class);
189 $this->expectExceptionMessage('Expected a string after %require but found integer at 1:10');
190
191 (new DeclarationParser())->directive($stream->next(), $stream);
192 }
193
194 public function testDirectiveRejectsARuleModifier(): void
195 {
196 $stream = new TokenStream([new Token(TokenKind::Directive, 'prec', new Location(1, 1), '%prec'), new Token(TokenKind::End, '', new Location(1, 6))]);
197
198 $this->expectException(SyntaxException::class);
199 $this->expectExceptionMessage("Expected a declaration but found '%prec' at 1:1");
200
201 (new DeclarationParser())->directive($stream->next(), $stream);
202 }
203
204 public function testDefine(): void
205 {
206 $parser = new DeclarationParser();
207 $keyword = $parser->define(new TokenStream((new Scanner())->scan('api.pure full')), new Location(1, 1));
208 $string = $parser->define(new TokenStream((new Scanner())->scan('api.prefix = "yy"')), new Location(2, 1));
209 $code = $parser->define(new TokenStream((new Scanner())->scan('api.value.type {struct s}')), new Location(3, 1));
210 $bare = $parser->define(new TokenStream((new Scanner())->scan('parse.trace %%')), new Location(4, 1));
211
212 self::assertSame(['api.pure', 'full', DefineForm::Keyword], [$keyword->variable, $keyword->value, $keyword->form]);
213 self::assertSame(['api.prefix', 'yy', DefineForm::String], [$string->variable, $string->value, $string->form]);
214 self::assertSame(['api.value.type', 'struct s', DefineForm::Code], [$code->variable, $code->value, $code->form]);
215 self::assertSame(['parse.trace', null, null, '4:1'], [$bare->variable, $bare->value, $bare->form, (string) $bare->location()]);
216 }
217
218 public function testDefineRejectsAMissingVariable(): void
219 {
220 $this->expectException(SyntaxException::class);
221 $this->expectExceptionMessage("Expected a variable after %define but found '%%' at 1:1");
222
223 (new DeclarationParser())->define(new TokenStream((new Scanner())->scan('%%')), new Location(1, 1));
224 }
225
226 public function testCodes(): void
227 {
228 $stream = new TokenStream((new Scanner())->scan('{int a} {int b} %%'));
229
230 self::assertSame(['int a', 'int b'], (new DeclarationParser())->codes($stream, 'param'));
231 self::assertTrue($stream->is(TokenKind::Section));
232 }
233
234 public function testCodesRejectsAMissingBlock(): void
235 {
236 $this->expectException(SyntaxException::class);
237 $this->expectExceptionMessage("Expected braced code after %param but found '%%' at 1:1");
238
239 (new DeclarationParser())->codes(new TokenStream((new Scanner())->scan('%%')), 'param');
240 }
241
242 public function testDirectiveReadsNtermAsNonterminals(): void
243 {
244 $parser = new DeclarationParser();
245 $tokens = new TokenStream((new Scanner())->scan('%nterm <t> a b %%'));
246
247 $declaration = $parser->directive($tokens->next(), $tokens);
248
249 self::assertInstanceOf(SymbolDeclaration::class, $declaration);
250 self::assertSame(SymbolClass::Nonterminal, $declaration->class);
251 self::assertSame(['a', 'b'], array_map(static fn (SymbolEntry $entry): string => $entry->symbol->value, $declaration->entries));
252 }
253
254 public function testDirectiveRejectsANtermThatIsNotAnIdentifier(): void
255 {
256 $tokens = new TokenStream((new Scanner())->scan("%nterm 'c'"));
257
258 $this->expectException(SyntaxException::class);
259 $this->expectExceptionMessage('Expected an identifier but found character literal at 1:8');
260
261 (new DeclarationParser())->directive($tokens->next(), $tokens);
262 }
263}
264