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