1<?php
2
3/**
4 * Hoa
5 *
6 *
7 * @license
8 *
9 * New BSD License
10 *
11 * Copyright © 2007-2017, Hoa community. All rights reserved.
12 *
13 * Redistribution and use in source and binary forms, with or without
14 * modification, are permitted provided that the following conditions are met:
15 *     * Redistributions of source code must retain the above copyright
16 *       notice, this list of conditions and the following disclaimer.
17 *     * Redistributions in binary form must reproduce the above copyright
18 *       notice, this list of conditions and the following disclaimer in the
19 *       documentation and/or other materials provided with the distribution.
20 *     * Neither the name of the Hoa nor the names of its contributors may be
21 *       used to endorse or promote products derived from this software without
22 *       specific prior written permission.
23 *
24 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
25 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
26 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
27 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE
28 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
29 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
30 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
31 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
32 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
33 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
34 * POSSIBILITY OF SUCH DAMAGE.
35 */
36
37namespace Hoa\Compiler\Test\Unit\Llk;
38
39use Hoa\Compiler as LUT;
40use Hoa\Compiler\Llk\Llk as SUT;
41use Hoa\File;
42use Hoa\Test;
43
44/**
45 * Class \Hoa\Compiler\Test\Unit\Llk\Llk.
46 *
47 * Test suite of the LL(k) helper class.
48 *
49 * @copyright  Copyright © 2007-2017 Hoa community
50 * @license    New BSD License
51 */
52class Llk extends Test\Unit\Suite
53{
54    public function case_load_empty()
55    {
56        $this
57            ->given($stream = new File\ReadWrite('hoa://Test/Vfs/Empty.pp?type=file'))
58            ->exception(function () use ($stream) {
59                SUT::load($stream);
60            })
61                ->isInstanceOf(LUT\Exception::class)
62                ->hasMessage(
63                    'The grammar is empty: Nothing to read on the stream ' .
64                    'hoa://Test/Vfs/Empty.pp?type=file.'
65                );
66    }
67
68    public function case_load()
69    {
70        $this
71            ->given(
72                $stream = new File\ReadWrite('hoa://Test/Vfs/Grammar.pp?type=file'),
73                $stream->writeAll(
74                    '%pragma  hello world' . "\n" .
75                    '%token   foobar bazqux' . "\n" .
76                    'ruleA:' . "\n" .
77                    '    <foobar>'
78                ),
79
80                $_ruleA = new LUT\Llk\Rule\Token('ruleA', 'foobar', null, -1, true),
81                $_ruleA->setPPRepresentation(' <foobar>')
82            )
83            ->when($result = SUT::load($stream))
84            ->then
85                ->object($result)
86                    ->isInstanceOf(LUT\Llk\Parser::class)
87                ->array($result->getPragmas())
88                    ->isEqualTo([
89                        'hello' => 'world'
90                    ])
91                ->array($result->getTokens())
92                    ->isEqualTo([
93                        'default' => [
94                            'foobar' => 'bazqux'
95                        ]
96                    ])
97                ->array($result->getRules())
98                    ->isEqualTo([
99                        'ruleA' => $_ruleA
100                    ]);
101    }
102
103    public function case_save()
104    {
105        $this
106            ->given(
107                $stream = new File\Read('hoa://Library/Compiler/Llk/Llk.pp'),
108                $parser = SUT::load($stream)
109            )
110            ->when($result = SUT::save($parser, 'Foobar'))
111            ->then
112                ->string($result)
113                    ->isEqualTo(<<<'OUTPUT'
114class Foobar extends \Hoa\Compiler\Llk\Parser
115{
116    public function __construct()
117    {
118        parent::__construct(
119            [
120                'default' => [
121                    'skip' => '\s',
122                    'or' => '\|',
123                    'zero_or_one' => '\?',
124                    'one_or_more' => '\+',
125                    'zero_or_more' => '\*',
126                    'n_to_m' => '\{[0-9]+,[0-9]+\}',
127                    'zero_to_m' => '\{,[0-9]+\}',
128                    'n_or_more' => '\{[0-9]+,\}',
129                    'exactly_n' => '\{[0-9]+\}',
130                    'token' => '[a-zA-Z_][a-zA-Z0-9_]*',
131                    'skipped' => '::',
132                    'kept_' => '<',
133                    '_kept' => '>',
134                    'named' => '\(\)',
135                    'node' => '#[a-zA-Z_][a-zA-Z0-9_]*(:[mM])?',
136                    'capturing_' => '\(',
137                    '_capturing' => '\)',
138                    'unification_' => '\[',
139                    'unification' => '[0-9]+',
140                    '_unification' => '\]',
141                ],
142            ],
143            [
144                0 => new \Hoa\Compiler\Llk\Rule\Concatenation(0, ['choice'], null),
145                'rule' => new \Hoa\Compiler\Llk\Rule\Concatenation('rule', [0], '#rule'),
146                2 => new \Hoa\Compiler\Llk\Rule\Token(2, 'or', null, -1, false),
147                3 => new \Hoa\Compiler\Llk\Rule\Concatenation(3, [2, 'concatenation'], '#choice'),
148                4 => new \Hoa\Compiler\Llk\Rule\Repetition(4, 0, -1, 3, null),
149                'choice' => new \Hoa\Compiler\Llk\Rule\Concatenation('choice', ['concatenation', 4], null),
150                6 => new \Hoa\Compiler\Llk\Rule\Concatenation(6, ['repetition'], '#concatenation'),
151                7 => new \Hoa\Compiler\Llk\Rule\Repetition(7, 0, -1, 6, null),
152                'concatenation' => new \Hoa\Compiler\Llk\Rule\Concatenation('concatenation', ['repetition', 7], null),
153                9 => new \Hoa\Compiler\Llk\Rule\Concatenation(9, ['quantifier'], '#repetition'),
154                10 => new \Hoa\Compiler\Llk\Rule\Repetition(10, 0, 1, 9, null),
155                11 => new \Hoa\Compiler\Llk\Rule\Token(11, 'node', null, -1, true),
156                12 => new \Hoa\Compiler\Llk\Rule\Repetition(12, 0, 1, 11, null),
157                'repetition' => new \Hoa\Compiler\Llk\Rule\Concatenation('repetition', ['simple', 10, 12], null),
158                14 => new \Hoa\Compiler\Llk\Rule\Token(14, 'capturing_', null, -1, false),
159                15 => new \Hoa\Compiler\Llk\Rule\Token(15, '_capturing', null, -1, false),
160                16 => new \Hoa\Compiler\Llk\Rule\Concatenation(16, [14, 'choice', 15], null),
161                17 => new \Hoa\Compiler\Llk\Rule\Token(17, 'skipped', null, -1, false),
162                18 => new \Hoa\Compiler\Llk\Rule\Token(18, 'token', null, -1, true),
163                19 => new \Hoa\Compiler\Llk\Rule\Token(19, 'unification_', null, -1, false),
164                20 => new \Hoa\Compiler\Llk\Rule\Token(20, 'unification', null, -1, true),
165                21 => new \Hoa\Compiler\Llk\Rule\Token(21, '_unification', null, -1, false),
166                22 => new \Hoa\Compiler\Llk\Rule\Concatenation(22, [19, 20, 21], null),
167                23 => new \Hoa\Compiler\Llk\Rule\Repetition(23, 0, 1, 22, null),
168                24 => new \Hoa\Compiler\Llk\Rule\Token(24, 'skipped', null, -1, false),
169                25 => new \Hoa\Compiler\Llk\Rule\Concatenation(25, [17, 18, 23, 24], '#skipped'),
170                26 => new \Hoa\Compiler\Llk\Rule\Token(26, 'kept_', null, -1, false),
171                27 => new \Hoa\Compiler\Llk\Rule\Token(27, 'token', null, -1, true),
172                28 => new \Hoa\Compiler\Llk\Rule\Token(28, 'unification_', null, -1, false),
173                29 => new \Hoa\Compiler\Llk\Rule\Token(29, 'unification', null, -1, true),
174                30 => new \Hoa\Compiler\Llk\Rule\Token(30, '_unification', null, -1, false),
175                31 => new \Hoa\Compiler\Llk\Rule\Concatenation(31, [28, 29, 30], null),
176                32 => new \Hoa\Compiler\Llk\Rule\Repetition(32, 0, 1, 31, null),
177                33 => new \Hoa\Compiler\Llk\Rule\Token(33, '_kept', null, -1, false),
178                34 => new \Hoa\Compiler\Llk\Rule\Concatenation(34, [26, 27, 32, 33], '#kept'),
179                35 => new \Hoa\Compiler\Llk\Rule\Token(35, 'token', null, -1, true),
180                36 => new \Hoa\Compiler\Llk\Rule\Token(36, 'named', null, -1, false),
181                37 => new \Hoa\Compiler\Llk\Rule\Concatenation(37, [35, 36], null),
182                'simple' => new \Hoa\Compiler\Llk\Rule\Choice('simple', [16, 25, 34, 37], null),
183                39 => new \Hoa\Compiler\Llk\Rule\Token(39, 'zero_or_one', null, -1, true),
184                40 => new \Hoa\Compiler\Llk\Rule\Token(40, 'one_or_more', null, -1, true),
185                41 => new \Hoa\Compiler\Llk\Rule\Token(41, 'zero_or_more', null, -1, true),
186                42 => new \Hoa\Compiler\Llk\Rule\Token(42, 'n_to_m', null, -1, true),
187                43 => new \Hoa\Compiler\Llk\Rule\Token(43, 'n_or_more', null, -1, true),
188                44 => new \Hoa\Compiler\Llk\Rule\Token(44, 'exactly_n', null, -1, true),
189                'quantifier' => new \Hoa\Compiler\Llk\Rule\Choice('quantifier', [39, 40, 41, 42, 43, 44], null),
190            ],
191            [
192            ]
193        );
194
195        $this->getRule('rule')->setDefaultId('#rule');
196        $this->getRule('rule')->setPPRepresentation(' choice()');
197        $this->getRule('choice')->setPPRepresentation(' concatenation() ( ::or:: concatenation() #choice )*');
198        $this->getRule('concatenation')->setPPRepresentation(' repetition() ( repetition() #concatenation )*');
199        $this->getRule('repetition')->setPPRepresentation(' simple() ( quantifier() #repetition )? <node>?');
200        $this->getRule('simple')->setPPRepresentation(' ::capturing_:: choice() ::_capturing:: | ::skipped:: <token> ( ::unification_:: <unification> ::_unification:: )? ::skipped:: #skipped | ::kept_:: <token> ( ::unification_:: <unification> ::_unification:: )? ::_kept:: #kept | <token> ::named::');
201        $this->getRule('quantifier')->setPPRepresentation(' <zero_or_one> | <one_or_more> | <zero_or_more> | <n_to_m> | <n_or_more> | <exactly_n>');
202    }
203}
204
205OUTPUT
206);
207    }
208
209    public function case_parse_tokens()
210    {
211        $this
212            ->given(
213                $pp =
214                    '%token  foobar1            bazqux1' . "\n" .
215                    '%token  sourceNS1:foobar2  bazqux2' . "\n" .
216                    '%token  sourceNS2:foobar3  bazqux3  -> destinationNS' . "\n" .
217                    '%token  foobar4            barqux4  -> destinationNS'
218            )
219            ->when($result = SUT::parsePP($pp, $tokens, $rules, $pragmas, 'streamFoo'))
220            ->then
221                ->variable($result)
222                    ->isNull()
223                ->array($tokens)
224                    ->isEqualTo([
225                        'default' => [
226                            'foobar1'               => 'bazqux1',
227                            'foobar4:destinationNS' => 'barqux4'
228                        ],
229                        'sourceNS1' => [
230                            'foobar2' => 'bazqux2'
231                        ],
232                        'sourceNS2' => [
233                            'foobar3:destinationNS' => 'bazqux3'
234                        ]
235                    ])
236                ->array($rules)
237                    ->isEmpty()
238                ->array($pragmas)
239                    ->isEmpty();
240    }
241
242    public function case_parse_skip_tokens()
243    {
244        $this
245            ->given(
246                $pp =
247                '%skip  foobar1            bazqux1' . "\n" .
248                '%skip  foobar2            bazqux2' . "\n" .
249                '%skip  foobar3            bazqux3' . "\n" .
250                '%skip  sourceNS1:foobar4  bazqux4' . "\n" .
251                '%skip  sourceNS1:foobar5  bazqux5' . "\n" .
252                '%skip  sourceNS2:foobar6  bazqux6' . "\n"
253            )
254            ->when($result = SUT::parsePP($pp, $tokens, $rules, $pragmas, 'streamFoo'))
255            ->then
256                ->variable($result)
257                    ->isNull()
258                ->array($tokens)
259                    ->isEqualTo([
260                        'default' => [
261                            'skip' => '(?:(?:bazqux1|bazqux2)|bazqux3)',
262                        ],
263                        'sourceNS1' => [
264                            'skip' => '(?:bazqux4|bazqux5)'
265                        ],
266                        'sourceNS2' => [
267                            'skip' => 'bazqux6'
268                        ]
269                    ])
270                ->array($rules)
271                    ->isEmpty()
272                ->array($pragmas)
273                    ->isEmpty();
274    }
275
276    public function case_parse_pragmas()
277    {
278        $this
279            ->given(
280                $pp =
281                    '%pragma  truly   true' . "\n" .
282                    '%pragma  falsy   false' . "\n" .
283                    '%pragma  numby   42' . "\n" .
284                    '%pragma  foobar  hello' . "\n" .
285                    '%pragma  bazqux  "world!"  ' . "\n"
286            )
287            ->when($result = SUT::parsePP($pp, $tokens, $rules, $pragmas, 'streamFoo'))
288            ->then
289                ->variable($result)
290                    ->isNull()
291                ->array($tokens)
292                    ->isEqualTo(['default' => []])
293                ->array($rules)
294                    ->isEmpty()
295                ->array($pragmas)
296                    ->isIdenticalTo([
297                        'truly'  => true,
298                        'falsy'  => false,
299                        'numby'  => 42,
300                        'foobar' => 'hello',
301                        'bazqux' => '"world!"'
302                    ]);
303    }
304
305    public function case_unrecognized_instructions()
306    {
307        $this
308            ->given(
309                $pp =
310                    '// shift line' . "\n" .
311                    '%foobar baz qux' . "\n"
312            )
313            ->exception(function () use ($pp) {
314                SUT::parsePP($pp, $tokens, $rules, $pragmas, 'streamFoo');
315            })
316                ->isInstanceOf(LUT\Exception::class)
317                ->hasMessage(
318                    'Unrecognized instructions:' . "\n" .
319                    '    %foobar baz qux' . "\n" .
320                    'in file streamFoo at line 2.'
321                );
322    }
323
324    public function case_parse_rules()
325    {
326        $this
327            ->given(
328                $pp =
329                    'ruleA:' . "\n" .
330                    ' single space' . "\n" .
331                    ' single space' . "\n" .
332                    'ruleB:' . "\n" .
333                    '    many spaces' . "\n" .
334                    "\t" . 'single tab' . "\n" .
335                    'ruleC:' . "\n" .
336                    "\t\t" . 'many tabs' . "\n"
337            )
338            ->when($result = SUT::parsePP($pp, $tokens, $rules, $pragmas, 'streamFoo'))
339            ->then
340                ->variable($result)
341                    ->isNull()
342                ->array($tokens)
343                    ->isEqualTo(['default' => []])
344                ->array($rules)
345                    ->isEqualTo([
346                        'ruleA' => ' single space single space',
347                        'ruleB' => ' many spaces single tab',
348                        'ruleC' => ' many tabs'
349                    ])
350                ->array($pragmas)
351                    ->isEmpty();
352    }
353
354    public function case_parse_skip_comments()
355    {
356        $this
357            ->given(
358                $pp =
359                    '// Hello,' . "\n" .
360                    '//   World!'
361            )
362            ->when($result = SUT::parsePP($pp, $tokens, $rules, $pragmas, 'streamFoo'))
363            ->then
364                ->variable($result)
365                    ->isNull()
366                ->array($tokens)
367                    ->isEqualTo(['default' => []])
368                ->array($rules)
369                    ->isEmpty()
370                ->array($pragmas)
371                    ->isEmpty();
372    }
373}
374