packages/sql-faker/tests/Unit/Compiler/Lemon/LemonGrammarCompilerTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Compiler\Lemon;
6
7use LemonParser\Ast\CodeBlock;
8use LemonParser\Ast\Declaration\ArgumentForm;
9use LemonParser\Ast\Declaration\Associativity;
10use LemonParser\Ast\Declaration\Declaration;
11use LemonParser\Ast\Declaration\Destructor;
12use LemonParser\Ast\Declaration\Directive;
13use LemonParser\Ast\Declaration\DirectiveKeyword;
14use LemonParser\Ast\Declaration\Fallback;
15use LemonParser\Ast\Declaration\PrecedenceDeclaration;
16use LemonParser\Ast\Declaration\TokenClass;
17use LemonParser\Ast\Declaration\TokenDeclaration;
18use LemonParser\Ast\Declaration\TypeDeclaration;
19use LemonParser\Ast\Declaration\Wildcard;
20use LemonParser\Ast\GrammarFile;
21use LemonParser\Ast\Location;
22use LemonParser\Ast\RhsItem;
23use LemonParser\Ast\Rule;
24use LemonParser\Ast\Symbol;
25use LemonParser\Parser;
26use LemonParser\Preprocessor\Condition;
27use LemonParser\Preprocessor\Exclusion;
28use LemonParser\Preprocessor\Preprocessor;
29use LemonParser\Printer\DeclarationPrinter;
30use LemonParser\Printer\Printer;
31use LemonParser\Printer\RulePrinter;
32use LemonParser\Scanner\CodeReader;
33use LemonParser\Scanner\Cursor;
34use LemonParser\Scanner\Scanner;
35use LemonParser\Scanner\Token;
36use LemonParser\Scanner\TokenKind;
37use LemonParser\Syntax\DeclarationReader;
38use LemonParser\Syntax\GrammarReader;
39use LemonParser\Syntax\RuleReader;
40use LemonParser\Syntax\SymbolListReader;
41use LemonParser\Syntax\SymbolRegistry;
42use LemonParser\Syntax\TokenStream;
43use LemonParser\SyntaxException;
44use PHPUnit\Framework\Attributes\CoversClass;
45use PHPUnit\Framework\Attributes\Small;
46use PHPUnit\Framework\Attributes\UsesClass;
47use PHPUnit\Framework\TestCase;
48use SqlFaker\Compiler\GrammarParseException;
49use SqlFaker\Compiler\Lemon\LemonGrammarCompiler;
50use SqlFaker\Grammar\Model\Grammar;
51use SqlFaker\Grammar\Model\NonTerminal;
52use SqlFaker\Grammar\Model\Production;
53use SqlFaker\Grammar\Model\ProductionRule;
54use SqlFaker\Grammar\Model\Terminal;
55
56#[CoversClass(LemonGrammarCompiler::class)]
57#[UsesClass(CodeBlock::class)]
58#[UsesClass(ArgumentForm::class)]
59#[UsesClass(Associativity::class)]
60#[UsesClass(Declaration::class)]
61#[UsesClass(Destructor::class)]
62#[UsesClass(Directive::class)]
63#[UsesClass(DirectiveKeyword::class)]
64#[UsesClass(Fallback::class)]
65#[UsesClass(PrecedenceDeclaration::class)]
66#[UsesClass(TokenClass::class)]
67#[UsesClass(TokenDeclaration::class)]
68#[UsesClass(TypeDeclaration::class)]
69#[UsesClass(Wildcard::class)]
70#[UsesClass(GrammarFile::class)]
71#[UsesClass(Location::class)]
72#[UsesClass(RhsItem::class)]
73#[UsesClass(Rule::class)]
74#[UsesClass(Symbol::class)]
75#[UsesClass(Parser::class)]
76#[UsesClass(Condition::class)]
77#[UsesClass(Exclusion::class)]
78#[UsesClass(Preprocessor::class)]
79#[UsesClass(DeclarationPrinter::class)]
80#[UsesClass(Printer::class)]
81#[UsesClass(RulePrinter::class)]
82#[UsesClass(CodeReader::class)]
83#[UsesClass(Cursor::class)]
84#[UsesClass(Scanner::class)]
85#[UsesClass(Token::class)]
86#[UsesClass(TokenKind::class)]
87#[UsesClass(SyntaxException::class)]
88#[UsesClass(DeclarationReader::class)]
89#[UsesClass(GrammarReader::class)]
90#[UsesClass(RuleReader::class)]
91#[UsesClass(SymbolListReader::class)]
92#[UsesClass(SymbolRegistry::class)]
93#[UsesClass(TokenStream::class)]
94#[UsesClass(GrammarParseException::class)]
95#[UsesClass(Grammar::class)]
96#[UsesClass(NonTerminal::class)]
97#[UsesClass(Production::class)]
98#[UsesClass(ProductionRule::class)]
99#[UsesClass(Terminal::class)]
100#[Small]
101final class LemonGrammarCompilerTest extends TestCase
102{
103 public function testCompile(): void
104 {
105 $grammar = (new LemonGrammarCompiler())->compile("%token_class id ID|INDEXED.\n%left PLUS MINUS.\nexpr(A) ::= expr(B) PLUS|MINUS term(C). [PLUS] { A = B + C; }\nexpr ::= term.\nterm ::= NUM.\nterm ::= id.\nexpr ::= .\n");
106
107 self::assertSame('expr', $grammar->startSymbol);
108 self::assertSame(['expr', 'term', 'id'], array_keys($grammar->ruleMap));
109 $spell = static fn (Production $production): string => implode(' ', array_map(static fn (\SqlFaker\Grammar\Model\Symbol $symbol): string => ($symbol instanceof Terminal ? 'T:' : 'N:') . $symbol->value(), $production->symbols));
110 self::assertSame(['N:expr T:PLUS N:term', 'N:expr T:MINUS N:term', 'N:term', ''], array_map($spell, $grammar->ruleMap['expr']->alternatives));
111 self::assertSame(['T:NUM', 'N:id'], array_map($spell, $grammar->ruleMap['term']->alternatives));
112 self::assertSame(['T:ID', 'T:INDEXED'], array_map($spell, $grammar->ruleMap['id']->alternatives));
113 }
114
115 public function testCompileSettlesConditionalRegions(): void
116 {
117 $source = "%ifndef OMIT\n cmd ::= EXTRA.\n%endif\ncmd ::= SELECT.\n";
118 $compiler = new LemonGrammarCompiler();
119
120 self::assertCount(2, $compiler->compile($source)->ruleMap['cmd']->alternatives);
121 self::assertCount(1, $compiler->compile($source, ['OMIT'])->ruleMap['cmd']->alternatives);
122 }
123
124 public function testCompileRejectsAFileWithoutRules(): void
125 {
126 $this->expectException(GrammarParseException::class);
127
128 (new LemonGrammarCompiler())->compile("%token A.\n");
129 }
130
131 public function testExpand(): void
132 {
133 $file = (new Parser())->parse("s ::= A|B c(X) D|E.\nt ::= .\n");
134 $compiler = new LemonGrammarCompiler();
135
136 self::assertSame([['A', 'c', 'D'], ['A', 'c', 'E'], ['B', 'c', 'D'], ['B', 'c', 'E']], $compiler->expand($file->rules()[0]));
137 self::assertSame([[]], $compiler->expand($file->rules()[1]));
138 }
139
140 public function testTerminals(): void
141 {
142 $file = (new Parser())->parse("%token A.\n%left B.\n%fallback C D.\n%wildcard Any.\n%token_class cls E|F.\ns ::= A G h Mixed cls.\nh ::= Any.\n");
143
144 self::assertSame(['Any' => true, 'A' => true, 'G' => true], (new LemonGrammarCompiler())->terminals($file));
145 }
146
147
148 public function testIsTokenName(): void
149 {
150 self::assertTrue(LemonGrammarCompiler::isTokenName('ID'));
151 self::assertTrue(LemonGrammarCompiler::isTokenName('JOIN_KW2'));
152 self::assertFalse(LemonGrammarCompiler::isTokenName('expr'));
153 self::assertFalse(LemonGrammarCompiler::isTokenName('Mixed'));
154 self::assertFalse(LemonGrammarCompiler::isTokenName('aID'));
155 self::assertFalse(LemonGrammarCompiler::isTokenName('IDa'));
156 self::assertFalse(LemonGrammarCompiler::isTokenName(''));
157 }
158}
159