packages/lemon-parser/tests/Unit/Syntax/SymbolListReaderTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Syntax;
6
7use LemonParser\Ast\Location;
8use LemonParser\Ast\Symbol;
9use LemonParser\Scanner\CodeReader;
10use LemonParser\Scanner\Cursor;
11use LemonParser\Scanner\Scanner;
12use LemonParser\Scanner\Token;
13use LemonParser\Scanner\TokenKind;
14use LemonParser\Syntax\SymbolListReader;
15use LemonParser\Syntax\SymbolRegistry;
16use LemonParser\Syntax\TokenStream;
17use LemonParser\SyntaxException;
18use PHPUnit\Framework\Attributes\CoversClass;
19use PHPUnit\Framework\Attributes\Small;
20use PHPUnit\Framework\Attributes\UsesClass;
21use PHPUnit\Framework\TestCase;
22
23#[CoversClass(SymbolListReader::class)]
24#[UsesClass(CodeReader::class)]
25#[UsesClass(Cursor::class)]
26#[UsesClass(Location::class)]
27#[UsesClass(Scanner::class)]
28#[UsesClass(SymbolRegistry::class)]
29#[UsesClass(SyntaxException::class)]
30#[UsesClass(Token::class)]
31#[UsesClass(TokenKind::class)]
32#[UsesClass(TokenStream::class)]
33#[UsesClass(Symbol::class)]
34#[Small]
35final class SymbolListReaderTest extends TestCase
36{
37    public function testRanked(): void
38    {
39        $registry = new SymbolRegistry();
40
41        $symbols = (new SymbolListReader())->ranked(new TokenStream((new Scanner())->scan('PLUS MINUS. x')), $registry);
42
43        self::assertSame(['PLUS', 'MINUS'], array_map(static fn (Symbol $symbol): string => $symbol->name, $symbols));
44        self::assertTrue($registry->isKnown('MINUS'));
45    }
46
47    public function testRankedRejectsANonterminal(): void
48    {
49        $this->expectException(SyntaxException::class);
50        $this->expectExceptionMessage('Can\'t assign a precedence to "expr". at 1:6');
51
52        (new SymbolListReader())->ranked(new TokenStream((new Scanner())->scan('PLUS expr.')), new SymbolRegistry());
53    }
54
55    public function testFallbacks(): void
56    {
57        $registry = new SymbolRegistry();
58
59        $symbols = (new SymbolListReader())->fallbacks(new TokenStream((new Scanner())->scan('ID ABORT AFTER.')), $registry);
60
61        self::assertSame(['ID', 'ABORT', 'AFTER'], array_map(static fn (Symbol $symbol): string => $symbol->name, $symbols));
62        self::assertTrue($registry->isKnown('ID'));
63    }
64
65    public function testFallbacksRejectsANonterminal(): void
66    {
67        $this->expectException(SyntaxException::class);
68        $this->expectExceptionMessage('%fallback argument "id" should be a token at 1:1');
69
70        (new SymbolListReader())->fallbacks(new TokenStream((new Scanner())->scan('id ABORT.')), new SymbolRegistry());
71    }
72
73    public function testFallbacksRejectsASecondFallbackForAToken(): void
74    {
75        $registry = new SymbolRegistry();
76        $reader = new SymbolListReader();
77        $reader->fallbacks(new TokenStream((new Scanner())->scan('ID ABORT.')), $registry);
78
79        $this->expectException(SyntaxException::class);
80        $this->expectExceptionMessage('More than one fallback assigned to token ABORT at 1:7');
81
82        $reader->fallbacks(new TokenStream((new Scanner())->scan('OTHER ABORT.')), $registry);
83    }
84
85    public function testTokens(): void
86    {
87        $registry = new SymbolRegistry();
88
89        $symbols = (new SymbolListReader())->tokens(new TokenStream((new Scanner())->scan('SEMI LP.')), $registry, 'token');
90
91        self::assertSame(['SEMI', 'LP'], array_map(static fn (Symbol $symbol): string => $symbol->name, $symbols));
92        self::assertTrue($registry->isKnown('LP'));
93    }
94
95    public function testTokensRejectsANonterminal(): void
96    {
97        $this->expectException(SyntaxException::class);
98        $this->expectExceptionMessage('%token argument "semi" should be a token at 1:1');
99
100        (new SymbolListReader())->tokens(new TokenStream((new Scanner())->scan('semi.')), new SymbolRegistry(), 'token');
101    }
102
103    public function testWildcard(): void
104    {
105        $reader = new SymbolListReader();
106
107        self::assertSame('ANY', $reader->wildcard(new TokenStream((new Scanner())->scan('ANY.')), new SymbolRegistry())?->name);
108        self::assertNull($reader->wildcard(new TokenStream((new Scanner())->scan('.')), new SymbolRegistry()));
109    }
110
111    public function testWildcardRejectsANonterminal(): void
112    {
113        $this->expectException(SyntaxException::class);
114        $this->expectExceptionMessage('%wildcard argument "any" should be a token at 1:1');
115
116        (new SymbolListReader())->wildcard(new TokenStream((new Scanner())->scan('any.')), new SymbolRegistry());
117    }
118
119    public function testWildcardRejectsASecondToken(): void
120    {
121        $this->expectException(SyntaxException::class);
122        $this->expectExceptionMessage('Extra wildcard to token: OTHER at 1:5');
123
124        (new SymbolListReader())->wildcard(new TokenStream((new Scanner())->scan('ANY OTHER.')), new SymbolRegistry());
125    }
126
127    public function testClassTokens(): void
128    {
129        $registry = new SymbolRegistry();
130
131        $symbols = (new SymbolListReader())->classTokens(new TokenStream((new Scanner())->scan('ID|INDEXED /JOIN_KW KEY.')), $registry);
132
133        self::assertSame(['ID', 'INDEXED', 'JOIN_KW', 'KEY'], array_map(static fn (Symbol $symbol): string => $symbol->name, $symbols));
134        self::assertTrue($registry->isKnown('JOIN_KW'));
135    }
136
137    public function testClassTokensRejectsANonterminal(): void
138    {
139        $this->expectException(SyntaxException::class);
140        $this->expectExceptionMessage('%token_class argument "|indexed" should be a token at 1:3');
141
142        (new SymbolListReader())->classTokens(new TokenStream((new Scanner())->scan('ID|indexed.')), new SymbolRegistry());
143    }
144
145    public function testUntilPeriod(): void
146    {
147        $stream = new TokenStream((new Scanner())->scan('A |B . C'));
148
149        $taken = (new SymbolListReader())->untilPeriod($stream);
150
151        self::assertSame(['A', 'B'], array_map(static fn (Token $token): string => $token->text, $taken));
152        self::assertSame('C', $stream->peek()->text);
153    }
154
155    public function testUntilPeriodRejectsTheEndOfTheFile(): void
156    {
157        $this->expectException(SyntaxException::class);
158        $this->expectExceptionMessage('Declaration is not terminated by "." before the end of the file. at 1:4');
159
160        (new SymbolListReader())->untilPeriod(new TokenStream((new Scanner())->scan('A B')));
161    }
162}
163