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