packages/lemon-parser/tests/Unit/Syntax/DeclarationReaderTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Syntax;
6
7use LemonParser\Ast\Declaration\ArgumentForm;
8use LemonParser\Ast\Declaration\Associativity;
9use LemonParser\Ast\Declaration\Destructor;
10use LemonParser\Ast\Declaration\Directive;
11use LemonParser\Ast\Declaration\DirectiveKeyword;
12use LemonParser\Ast\Declaration\Fallback;
13use LemonParser\Ast\Declaration\PrecedenceDeclaration;
14use LemonParser\Ast\Declaration\TokenClass;
15use LemonParser\Ast\Declaration\TokenDeclaration;
16use LemonParser\Ast\Declaration\TypeDeclaration;
17use LemonParser\Ast\Declaration\Wildcard;
18use LemonParser\Ast\Location;
19use LemonParser\Ast\Symbol;
20use LemonParser\Scanner\CodeReader;
21use LemonParser\Scanner\Cursor;
22use LemonParser\Scanner\Scanner;
23use LemonParser\Scanner\Token;
24use LemonParser\Scanner\TokenKind;
25use LemonParser\Syntax\DeclarationReader;
26use LemonParser\Syntax\SymbolListReader;
27use LemonParser\Syntax\SymbolRegistry;
28use LemonParser\Syntax\TokenStream;
29use LemonParser\SyntaxException;
30use PHPUnit\Framework\Attributes\CoversClass;
31use PHPUnit\Framework\Attributes\Small;
32use PHPUnit\Framework\Attributes\UsesClass;
33use PHPUnit\Framework\TestCase;
34
35#[CoversClass(DeclarationReader::class)]
36#[UsesClass(CodeReader::class)]
37#[UsesClass(Cursor::class)]
38#[UsesClass(Location::class)]
39#[UsesClass(Scanner::class)]
40#[UsesClass(SymbolRegistry::class)]
41#[UsesClass(SyntaxException::class)]
42#[UsesClass(Token::class)]
43#[UsesClass(TokenKind::class)]
44#[UsesClass(TokenStream::class)]
45#[UsesClass(ArgumentForm::class)]
46#[UsesClass(Associativity::class)]
47#[UsesClass(Destructor::class)]
48#[UsesClass(Directive::class)]
49#[UsesClass(DirectiveKeyword::class)]
50#[UsesClass(Fallback::class)]
51#[UsesClass(PrecedenceDeclaration::class)]
52#[UsesClass(Symbol::class)]
53#[UsesClass(SymbolListReader::class)]
54#[UsesClass(TokenClass::class)]
55#[UsesClass(TokenDeclaration::class)]
56#[UsesClass(TypeDeclaration::class)]
57#[UsesClass(Wildcard::class)]
58#[Small]
59final class DeclarationReaderTest extends TestCase
60{
61 public function testRead(): void
62 {
63 $reader = new DeclarationReader();
64 $registry = new SymbolRegistry();
65 $stream = new TokenStream((new Scanner())->scan('name Calc left PLUS MINUS. destructor expr { free($$); } type expr {Expr*} fallback ID ABORT. token SEMI. wildcard ANY. token_class id ID|INDEXED.'));
66 $at = new Location(1, 1);
67 $name = $reader->read($stream, $registry, $at);
68 $left = $reader->read($stream, $registry, $at);
69 $destructor = $reader->read($stream, $registry, $at);
70 $type = $reader->read($stream, $registry, $at);
71 $fallback = $reader->read($stream, $registry, $at);
72 $token = $reader->read($stream, $registry, $at);
73 $wildcard = $reader->read($stream, $registry, $at);
74 $class = $reader->read($stream, $registry, $at);
75
76 self::assertTrue($stream->eof());
77 self::assertInstanceOf(Directive::class, $name);
78 self::assertSame([DirectiveKeyword::Name, 'Calc', ArgumentForm::Word], [$name->keyword, $name->value, $name->form]);
79 self::assertInstanceOf(PrecedenceDeclaration::class, $left);
80 self::assertSame([Associativity::Left, 'PLUS', 'MINUS'], [$left->associativity, $left->symbols[0]->name, $left->symbols[1]->name]);
81 self::assertInstanceOf(Destructor::class, $destructor);
82 self::assertSame(['expr', ' free($$); ', ArgumentForm::Code], [$destructor->symbol->name, $destructor->value, $destructor->form]);
83 self::assertInstanceOf(TypeDeclaration::class, $type);
84 self::assertSame(['expr', 'Expr*'], [$type->symbol->name, $type->value]);
85 self::assertInstanceOf(Fallback::class, $fallback);
86 self::assertSame(['ID', 'ABORT'], [$fallback->fallback()?->name, $fallback->tokens()[0]->name]);
87 self::assertInstanceOf(TokenDeclaration::class, $token);
88 self::assertSame('SEMI', $token->symbols[0]->name);
89 self::assertInstanceOf(Wildcard::class, $wildcard);
90 self::assertSame('ANY', $wildcard->symbol?->name);
91 self::assertInstanceOf(TokenClass::class, $class);
92 self::assertSame(['id', 'ID', 'INDEXED'], [$class->name->name, $class->tokens[0]->name, $class->tokens[1]->name]);
93 }
94
95 public function testReadRejectsAKeywordThatIsNotAWord(): void
96 {
97 $this->expectException(SyntaxException::class);
98 $this->expectExceptionMessage('Illegal declaration keyword: "1". at 1:1');
99
100 (new DeclarationReader())->read(new TokenStream((new Scanner())->scan('1')), new SymbolRegistry(), new Location(1, 1));
101 }
102
103 public function testReadRejectsAnUnknownKeyword(): void
104 {
105 $this->expectException(SyntaxException::class);
106 $this->expectExceptionMessage('Unknown declaration keyword: "%tokens". at 1:1');
107
108 (new DeclarationReader())->read(new TokenStream((new Scanner())->scan('tokens A.')), new SymbolRegistry(), new Location(1, 1));
109 }
110
111 public function testArgument(): void
112 {
113 $reader = new DeclarationReader();
114 $keyword = new Token(TokenKind::Word, 'name', new Location(1, 1), 'name');
115
116 self::assertSame([' x ', ArgumentForm::Code], $reader->argument(new TokenStream((new Scanner())->scan('{ x }')), $keyword));
117 self::assertSame(['x y', ArgumentForm::String], $reader->argument(new TokenStream((new Scanner())->scan('"x y"')), $keyword));
118 self::assertSame(['100', ArgumentForm::Word], $reader->argument(new TokenStream((new Scanner())->scan('100')), $keyword));
119 }
120
121 public function testArgumentRejectsPunctuation(): void
122 {
123 $this->expectException(SyntaxException::class);
124 $this->expectExceptionMessage('Illegal argument to %name: . at 1:1');
125
126 (new DeclarationReader())->argument(new TokenStream((new Scanner())->scan('.')), new Token(TokenKind::Word, 'name', new Location(1, 1), 'name'));
127 }
128
129 public function testReadDestructor(): void
130 {
131 $registry = new SymbolRegistry();
132
133 $destructor = (new DeclarationReader())->readDestructor(new TokenStream((new Scanner())->scan('expr "free"')), $registry, new Location(2, 1));
134
135 self::assertSame(['expr', 'free', ArgumentForm::String, '2:1'], [$destructor->symbol->name, $destructor->value, $destructor->form, (string) $destructor->location()]);
136 self::assertTrue($registry->isKnown('expr'));
137 }
138
139 public function testReadDestructorRejectsAMissingSymbol(): void
140 {
141 $this->expectException(SyntaxException::class);
142 $this->expectExceptionMessage('Symbol name missing after %destructor keyword at 1:1');
143
144 (new DeclarationReader())->readDestructor(new TokenStream((new Scanner())->scan('{ x }')), new SymbolRegistry(), new Location(1, 1));
145 }
146
147 public function testReadType(): void
148 {
149 $type = (new DeclarationReader())->readType(new TokenStream((new Scanner())->scan('expr {Expr*}')), new SymbolRegistry(), new Location(2, 1));
150
151 self::assertSame(['expr', 'Expr*', ArgumentForm::Code], [$type->symbol->name, $type->value, $type->form]);
152 }
153
154 public function testReadTypeRejectsAMissingSymbol(): void
155 {
156 $this->expectException(SyntaxException::class);
157 $this->expectExceptionMessage('Symbol name missing after %type keyword at 1:1');
158
159 (new DeclarationReader())->readType(new TokenStream((new Scanner())->scan('{ x }')), new SymbolRegistry(), new Location(1, 1));
160 }
161
162 public function testReadTypeRejectsASecondType(): void
163 {
164 $registry = new SymbolRegistry();
165 $reader = new DeclarationReader();
166 $reader->readType(new TokenStream((new Scanner())->scan('expr {Expr*}')), $registry, new Location(1, 1));
167
168 $this->expectException(SyntaxException::class);
169 $this->expectExceptionMessage('Symbol %type "expr" already defined at 1:1');
170
171 $reader->readType(new TokenStream((new Scanner())->scan('expr {Expr*}')), $registry, new Location(2, 1));
172 }
173
174 public function testReadTokenClass(): void
175 {
176 $registry = new SymbolRegistry();
177
178 $class = (new DeclarationReader())->readTokenClass(new TokenStream((new Scanner())->scan('id ID|INDEXED.')), $registry, new Location(2, 1));
179
180 self::assertSame(['id', 'ID', 'INDEXED'], [$class->name->name, $class->tokens[0]->name, $class->tokens[1]->name]);
181 self::assertTrue($registry->isKnown('id'));
182 }
183
184 public function testReadTokenClassRejectsATerminalName(): void
185 {
186 $this->expectException(SyntaxException::class);
187 $this->expectExceptionMessage('%token_class must be followed by an identifier: ID at 1:1');
188
189 (new DeclarationReader())->readTokenClass(new TokenStream((new Scanner())->scan('ID A.')), new SymbolRegistry(), new Location(1, 1));
190 }
191
192 public function testReadTokenClassRejectsAKnownName(): void
193 {
194 $registry = new SymbolRegistry();
195 $registry->see('id');
196
197 $this->expectException(SyntaxException::class);
198 $this->expectExceptionMessage('Symbol "id" already used at 1:1');
199
200 (new DeclarationReader())->readTokenClass(new TokenStream((new Scanner())->scan('id ID.')), $registry, new Location(1, 1));
201 }
202}
203