packages/bison-parser/tests/Unit/ParserTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit;
6
7use BisonParser\Ast\Declaration\Prologue;
8use BisonParser\Ast\Declaration\Symbols\Alias;
9use BisonParser\Ast\Declaration\Symbols\Associativity;
10use BisonParser\Ast\Declaration\Symbols\PrecedenceDeclaration;
11use BisonParser\Ast\Declaration\Symbols\SymbolClass;
12use BisonParser\Ast\Declaration\Symbols\SymbolDeclaration;
13use BisonParser\Ast\Declaration\Symbols\SymbolEntry;
14use BisonParser\Ast\Epilogue;
15use BisonParser\Ast\GrammarFile;
16use BisonParser\Ast\Location;
17use BisonParser\Ast\Rule\Action;
18use BisonParser\Ast\Rule\Alternative;
19use BisonParser\Ast\Rule\PrecItem;
20use BisonParser\Ast\Rule\Rule;
21use BisonParser\Ast\Rule\SymbolItem;
22use BisonParser\Ast\Symbol;
23use BisonParser\Ast\SymbolKind;
24use BisonParser\Parser;
25use BisonParser\Scanner\CodeReader;
26use BisonParser\Scanner\Cursor;
27use BisonParser\Scanner\Directives;
28use BisonParser\Scanner\Escapes;
29use BisonParser\Scanner\Scanner;
30use BisonParser\Scanner\Token;
31use BisonParser\Scanner\TokenKind;
32use BisonParser\Syntax\DeclarationParser;
33use BisonParser\Syntax\GrammarParser;
34use BisonParser\Syntax\RuleParser;
35use BisonParser\Syntax\SymbolListParser;
36use BisonParser\Syntax\TokenStream;
37use BisonParser\SyntaxException;
38use PHPUnit\Framework\Attributes\CoversClass;
39use PHPUnit\Framework\Attributes\Small;
40use PHPUnit\Framework\Attributes\UsesClass;
41use PHPUnit\Framework\TestCase;
42
43#[CoversClass(Parser::class)]
44#[UsesClass(Action::class)]
45#[UsesClass(Alias::class)]
46#[UsesClass(Alternative::class)]
47#[UsesClass(Associativity::class)]
48#[UsesClass(CodeReader::class)]
49#[UsesClass(Cursor::class)]
50#[UsesClass(DeclarationParser::class)]
51#[UsesClass(Directives::class)]
52#[UsesClass(Epilogue::class)]
53#[UsesClass(Escapes::class)]
54#[UsesClass(GrammarFile::class)]
55#[UsesClass(GrammarParser::class)]
56#[UsesClass(Location::class)]
57#[UsesClass(PrecItem::class)]
58#[UsesClass(PrecedenceDeclaration::class)]
59#[UsesClass(Prologue::class)]
60#[UsesClass(Rule::class)]
61#[UsesClass(RuleParser::class)]
62#[UsesClass(Scanner::class)]
63#[UsesClass(Symbol::class)]
64#[UsesClass(SymbolClass::class)]
65#[UsesClass(SymbolDeclaration::class)]
66#[UsesClass(SymbolEntry::class)]
67#[UsesClass(SymbolItem::class)]
68#[UsesClass(SymbolKind::class)]
69#[UsesClass(SymbolListParser::class)]
70#[UsesClass(SyntaxException::class)]
71#[UsesClass(Token::class)]
72#[UsesClass(TokenKind::class)]
73#[UsesClass(TokenStream::class)]
74#[Small]
75final class ParserTest extends TestCase
76{
77    public function testParse(): void
78    {
79        $file = (new Parser())->parse(implode("\n", [
80            '%{ #include <stdio.h> %}',
81            '%token <int> NUM 258 "number"',
82            "%left '+' '-'",
83            '%%',
84            "expr: expr '+' expr { \$\$ = \$1 + \$3; }",
85            '    | NUM %prec UMINUS',
86            '    ;',
87            '%%',
88            'int main() {}',
89        ]));
90
91        self::assertSame([Prologue::class, SymbolDeclaration::class, PrecedenceDeclaration::class], array_map(static fn (object $declaration): string => $declaration::class, $file->declarations));
92        self::assertSame('NUM', $file->rules()[0]->alternatives[1]->symbols()[0]->value);
93        self::assertSame([SymbolItem::class, SymbolItem::class, SymbolItem::class, Action::class], array_map(static fn (object $item): string => $item::class, $file->rules()[0]->alternatives[0]->items));
94        self::assertSame("\nint main() {}", $file->epilogue?->code);
95    }
96
97    public function testParseRejectsAGrammarBisonRejects(): void
98    {
99        $this->expectException(SyntaxException::class);
100        $this->expectExceptionMessage("Expected a declaration or '%%' but found 'expr' at 2:1");
101
102        (new Parser())->parse("%token NUM\nexpr: NUM;");
103    }
104}
105