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