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