packages/sql-parser/tests/Unit/Compiler/LemonGrammarReaderTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Compiler;
6
7use LemonParser\Ast\CodeBlock;
8use LemonParser\Ast\Declaration\ArgumentForm;
9use LemonParser\Ast\Declaration\Declaration;
10use LemonParser\Ast\Declaration\Destructor;
11use LemonParser\Ast\Declaration\Directive;
12use LemonParser\Ast\Declaration\DirectiveKeyword;
13use LemonParser\Ast\Declaration\Fallback;
14use LemonParser\Ast\Declaration\PrecedenceDeclaration;
15use LemonParser\Ast\Declaration\TokenClass;
16use LemonParser\Ast\Declaration\TokenDeclaration;
17use LemonParser\Ast\Declaration\TypeDeclaration;
18use LemonParser\Ast\Declaration\Wildcard;
19use LemonParser\Ast\GrammarFile;
20use LemonParser\Ast\Location;
21use LemonParser\Ast\RhsItem;
22use LemonParser\Ast\Symbol;
23use LemonParser\Parser;
24use LemonParser\Preprocessor\Condition;
25use LemonParser\Preprocessor\Exclusion;
26use LemonParser\Preprocessor\Preprocessor;
27use LemonParser\Printer\DeclarationPrinter;
28use LemonParser\Printer\Printer;
29use LemonParser\Printer\RulePrinter;
30use LemonParser\Scanner\CodeReader;
31use LemonParser\Scanner\Cursor;
32use LemonParser\Scanner\Scanner;
33use LemonParser\Scanner\Token;
34use LemonParser\Scanner\TokenKind;
35use LemonParser\Syntax\DeclarationReader;
36use LemonParser\Syntax\GrammarReader;
37use LemonParser\Syntax\RuleReader;
38use LemonParser\Syntax\SymbolListReader;
39use LemonParser\Syntax\SymbolRegistry;
40use LemonParser\Syntax\TokenStream;
41use LemonParser\SyntaxException;
42use PHPUnit\Framework\Attributes\CoversClass;
43use PHPUnit\Framework\Attributes\Small;
44use PHPUnit\Framework\Attributes\UsesClass;
45use PHPUnit\Framework\TestCase;
46use SqlParser\Compiler\LemonGrammarReader;
47use SqlParser\Grammar\Associativity;
48use SqlParser\Grammar\Grammar;
49use SqlParser\Grammar\GrammarBuilder;
50use SqlParser\Grammar\Precedence;
51use SqlParser\Grammar\PrecedencePolicy;
52use SqlParser\Grammar\Rule;
53use SqlParser\Grammar\SymbolTable;
54
55#[CoversClass(LemonGrammarReader::class)]
56#[UsesClass(CodeBlock::class)]
57#[UsesClass(ArgumentForm::class)]
58#[UsesClass(\LemonParser\Ast\Declaration\Associativity::class)]
59#[UsesClass(Declaration::class)]
60#[UsesClass(Destructor::class)]
61#[UsesClass(Directive::class)]
62#[UsesClass(DirectiveKeyword::class)]
63#[UsesClass(Fallback::class)]
64#[UsesClass(PrecedenceDeclaration::class)]
65#[UsesClass(TokenClass::class)]
66#[UsesClass(TokenDeclaration::class)]
67#[UsesClass(TypeDeclaration::class)]
68#[UsesClass(Wildcard::class)]
69#[UsesClass(GrammarFile::class)]
70#[UsesClass(Location::class)]
71#[UsesClass(RhsItem::class)]
72#[UsesClass(\LemonParser\Ast\Rule::class)]
73#[UsesClass(Symbol::class)]
74#[UsesClass(Parser::class)]
75#[UsesClass(Condition::class)]
76#[UsesClass(Exclusion::class)]
77#[UsesClass(Preprocessor::class)]
78#[UsesClass(DeclarationPrinter::class)]
79#[UsesClass(Printer::class)]
80#[UsesClass(RulePrinter::class)]
81#[UsesClass(CodeReader::class)]
82#[UsesClass(Cursor::class)]
83#[UsesClass(Scanner::class)]
84#[UsesClass(Token::class)]
85#[UsesClass(TokenKind::class)]
86#[UsesClass(SyntaxException::class)]
87#[UsesClass(DeclarationReader::class)]
88#[UsesClass(GrammarReader::class)]
89#[UsesClass(RuleReader::class)]
90#[UsesClass(SymbolListReader::class)]
91#[UsesClass(SymbolRegistry::class)]
92#[UsesClass(TokenStream::class)]
93#[UsesClass(Associativity::class)]
94#[UsesClass(Grammar::class)]
95#[UsesClass(GrammarBuilder::class)]
96#[UsesClass(Precedence::class)]
97#[UsesClass(PrecedencePolicy::class)]
98#[UsesClass(Rule::class)]
99#[UsesClass(SymbolTable::class)]
100#[Small]
101final class LemonGrammarReaderTest extends TestCase
102{
103 public function testRead(): void
104 {
105 $grammar = (new LemonGrammarReader())->read("%token_prefix TK_\n%token SEMI.\n%left PLUS MINUS.\n%fallback ID ABORT AFTER.\n%wildcard ANY.\n%token_class id ID|INDEXED.\n%start_symbol input\nexpr(A) ::= expr(B) PLUS|MINUS expr(C). [PLUS] { A = B + C; }\nexpr ::= NUM.\ninput ::= expr SEMI.\n");
106
107 self::assertSame('input', $grammar->symbols->name($grammar->startSymbol()));
108 self::assertSame(PrecedencePolicy::FirstRankedTerminal, $grammar->policy);
109 self::assertSame(['$accept', 'expr', 'expr', 'input'], array_map(static fn (Rule $rule): string => $grammar->symbols->name($rule->lhs), $grammar->rules));
110 self::assertSame(['expr', 'PLUS|MINUS', 'expr'], array_map(static fn (int $id): string => $grammar->symbols->name($id), $grammar->rules[1]->rhs));
111 self::assertSame('PLUS', $grammar->symbols->name($grammar->rules[1]->precedenceSymbol ?? -1));
112 self::assertSame('ANY', $grammar->symbols->name($grammar->wildcard ?? -1));
113 self::assertCount(2, $grammar->tokenClasses);
114 self::assertCount(2, $grammar->fallbacks);
115 self::assertSame(Associativity::Left, $grammar->precedenceOf($grammar->symbols->id('MINUS') ?? -1)?->associativity);
116 }
117
118 public function testReadSettlesConditionalRegions(): void
119 {
120 $source = "%ifndef OMIT\ncmd ::= EXTRA.\n%endif\ncmd ::= SELECT.\n";
121 $reader = new LemonGrammarReader();
122
123 self::assertCount(3, $reader->read($source)->rules);
124 self::assertCount(2, $reader->read($source, ['OMIT'])->rules);
125 }
126
127 public function testReadRejectsWhatLemonRejects(): void
128 {
129 $this->expectException(SyntaxException::class);
130
131 (new LemonGrammarReader())->read("expr ::= expr ? expr.\n");
132 }
133
134 public function testDeclaration(): void
135 {
136 $file = (new Parser())->parse("%token A.\n%right B.\n%fallback.\n%wildcard.\n%name Calc\n%start_symbol s\ns ::= A B.\n");
137 $reader = new LemonGrammarReader();
138 $builder = new GrammarBuilder();
139
140 $declarations = $file->declarations();
141
142 $reader->declaration($declarations[0], $builder);
143 $reader->declaration($declarations[1], $builder);
144 $reader->declaration($declarations[2], $builder);
145 $reader->declaration($declarations[3], $builder);
146 $reader->declaration($declarations[4], $builder);
147 $reader->declaration($declarations[5], $builder);
148 $builder->rule('s', ['A', 'B'], null);
149 $grammar = $builder->build();
150
151 self::assertTrue($builder->isTerminal('A'));
152 self::assertSame(Associativity::Right, $grammar->precedenceOf($grammar->symbols->id('B') ?? -1)?->associativity);
153 self::assertSame([], $grammar->fallbacks);
154 self::assertNull($grammar->wildcard);
155 self::assertSame('s', $grammar->symbols->name($grammar->startSymbol()));
156 }
157
158 public function testAssociativity(): void
159 {
160 $reader = new LemonGrammarReader();
161
162 self::assertSame(Associativity::Left, $reader->associativity(\LemonParser\Ast\Declaration\Associativity::Left));
163 self::assertSame(Associativity::Right, $reader->associativity(\LemonParser\Ast\Declaration\Associativity::Right));
164 self::assertSame(Associativity::NonAssoc, $reader->associativity(\LemonParser\Ast\Declaration\Associativity::NonAssoc));
165 }
166
167 public function testRule(): void
168 {
169 $file = (new Parser())->parse("s ::= A|B c A|B. [A]\nc ::= .\n");
170 $reader = new LemonGrammarReader();
171 $builder = new GrammarBuilder();
172
173 $reader->rule($file->rules()[0], $builder);
174 $reader->rule($file->rules()[1], $builder);
175 $grammar = $builder->build();
176
177 self::assertSame(['A|B', 'c', 'A|B'], array_map(static fn (int $id): string => $grammar->symbols->name($id), $grammar->rules[1]->rhs));
178 self::assertCount(1, $grammar->tokenClasses);
179 self::assertSame('A', $grammar->symbols->name($grammar->rules[1]->precedenceSymbol ?? -1));
180 self::assertSame([], $grammar->rules[2]->rhs);
181 }
182
183 public function testNames(): void
184 {
185 $symbols = [new Symbol('A', new Location(1, 1)), new Symbol('b', new Location(1, 3))];
186
187 self::assertSame(['A', 'b'], (new LemonGrammarReader())->names($symbols));
188 }
189}
190