Passed
Push — master ( af3941...7aa8d1 )
by Nikita
04:06
created

Parser::doCopy()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * This file is part of PHP-Yacc package.
4
 *
5
 * For the full copyright and license information, please view the LICENSE
6
 * file that was distributed with this source code.
7
 */
8
declare(strict_types=1);
9
10
namespace PhpYacc\Yacc;
11
12
use PhpYacc\Exception\ParseException;
13
use PhpYacc\Grammar\Context;
14
use PhpYacc\Grammar\Symbol;
15
use PhpYacc\Support\Utils;
16
17
/**
18
 * Class Parser.
19
 */
20
class Parser
21
{
22
    /**
23
     * @var Context
24
     */
25
    protected $context;
26
27
    /**
28
     * @var Lexer
29
     */
30
    protected $lexer;
31
32
    /**
33
     * @var MacroSet
34
     */
35
    protected $macros;
36
37
    /**
38
     * @var Symbol
39
     */
40
    protected $eofToken;
41
42
    /**
43
     * @var Symbol
44
     */
45
    protected $errorToken;
46
47
    /**
48
     * @var Symbol
49
     */
50
    protected $startPrime;
51
52
    /**
53
     * @var int
54
     */
55
    protected $currentPrecedence = 0;
56
57
    /**
58
     * Parser constructor.
59
     *
60
     * @param Lexer    $lexer
61
     * @param MacroSet $macros
62
     */
63
    public function __construct(Lexer $lexer, MacroSet $macros)
64
    {
65
        $this->lexer = $lexer;
66
        $this->macros = $macros;
67
    }
68
69
    /**
70
     * @param string       $code
71
     * @param Context|null $context
72
     *
73
     * @throws ParseException
74
     * @throws \PhpYacc\Exception\LexingException
75
     *
76
     * @return Context
77
     */
78
    public function parse(string $code, Context $context = null)
79
    {
80
        $this->context = $context ?: new Context();
81
82
        $this->lexer->startLexing($code, $this->context->filename);
83
84
        $this->doDeclaration();
85
        $this->doGrammar();
86
87
        $this->context->eofToken = $this->eofToken;
88
        $this->context->errorToken = $this->errorToken;
89
        $this->context->startPrime = $this->startPrime;
90
91
        $this->context->finish();
92
93
        return $this->context;
94
    }
95
96
    /**
97
     * @param array $symbols
98
     * @param int   $n
99
     * @param $delm
100
     * @param array $attribute
101
     *
102
     * @throws ParseException
103
     * @throws \PhpYacc\Exception\LexingException
104
     *
105
     * @return string
106
     */
107
    protected function copyAction(array $symbols, int $n, $delm, array $attribute): string
108
    {
109
        $tokens = [];
110
        $ct = 0;
111
112
        while (($token = $this->lexer->getRawToken())->getValue() !== $delm || $ct > 0) {
113
            switch ($token->getValue()) {
114
                case "\0":
115
                    throw ParseException::unexpected($token, Token::decode($delm));
116
                case '{':
117
                    $ct++;
118
                    break;
119
                case '}':
120
                    $ct--;
121
                    break;
122
            }
123
            $tokens[] = $token;
124
        }
125
126
        $expanded = $this->macros->apply($this->context, $symbols, $tokens, $n, $attribute);
127
128
        $action = \implode('', \array_map(function (Token $token) {
129
            return $token->getValue();
130
        }, $expanded));
131
132
        return $action;
133
    }
134
135
    /**
136
     * @throws ParseException
137
     * @throws \PhpYacc\Exception\LexingException
138
     */
139
    protected function doType()
140
    {
141
        $type = $this->getType();
142
        while (true) {
143
            if (($token = $this->lexer->getToken())->getValue() === ',') {
144
                continue;
145
            }
146
            if ($token->getType() !== Token::NAME && $token->getValue()[0] !== "'") {
147
                break;
148
            }
149
            $p = $this->context->internSymbol($token->getValue(), false);
150
            if ($type !== null) {
151
                $p->type = $type;
152
            }
153
        }
154
        $this->lexer->ungetToken();
155
    }
156
157
    /**
158
     * @throws ParseException
159
     * @throws \PhpYacc\Exception\LexingException
160
     */
161
    protected function doGrammar()
162
    {
163
        $attribute = [];
164
        $gbuffer = [null];
165
        $r = new Production('', 0);
166
167
        $r->body = [$this->startPrime];
168
        $this->context->addGram($r);
169
170
        $token = $this->lexer->getToken();
171
172
        while ($token->getType() !== Token::MARK && $token->getType() !== Token::EOF) {
173
            if ($token->getType() === Token::NAME) {
174
                if ($this->lexer->peek()->getValue()[0] === '@') {
175
                    $attribute[0] = $token->getValue();
176
                    $this->lexer->getToken();
177
                    $token = $this->lexer->getToken();
178
                } else {
179
                    $attribute[0] = null;
180
                }
181
                $gbuffer[0] = $this->context->internSymbol($token->getValue(), false);
182
                $attribute[1] = null;
183
                if ($gbuffer[0]->isterminal) {
184
                    throw new \RuntimeException("Nonterminal symbol expected: $token");
185
                } elseif (($tmp = $this->lexer->getToken())->getType() !== Token::COLON) {
186
                    throw new \RuntimeException("':' expected, $tmp found");
187
                }
188
                if ($this->context->startSymbol === null) {
189
                    $this->context->startSymbol = $gbuffer[0];
190
                }
191
            } elseif ($token->getValue()[0] === '|') {
192
                if (!$gbuffer[0]) {
193
                    throw new \RuntimeException("Syntax Error, unexpected $token");
194
                }
195
                $attribute[1] = null;
196
            } elseif ($token->getType() === Token::BEGININC) {
197
                $this->doCopy();
198
                $token = $this->lexer->getToken();
199
                continue;
200
            } else {
201
                throw new \RuntimeException("Syntax Error Unexpected $token");
202
            }
203
204
            $lastTerm = $this->startPrime;
205
            $action = null;
206
            $pos = 0;
207
            $i = 1;
208
            while (true) {
209
                $token = $this->lexer->getToken();
210
211
                if ($token->getValue()[0] === '=') {
212
                    $pos = $token->getLine();
213
                    if (($token = $this->lexer->getToken())->getValue()[0] === '{') {
214
                        $pos = $token->getLine();
215
                        $action = $this->copyAction($gbuffer, $i - 1, '}', $attribute);
216
                    } else {
217
                        $this->lexer->ungetToken();
218
                        $action = $this->copyAction($gbuffer, $i - 1, ';', $attribute);
219
                    }
220
                } elseif ($token->getValue()[0] === '{') {
221
                    $pos = $token->getLine();
222
                    $action = $this->copyAction($gbuffer, $i - 1, '}', $attribute);
223
                } elseif ($token->getType() === Token::PRECTOK) {
224
                    $lastTerm = $this->context->internSymbol($this->lexer->getToken()->getValue(), false);
225
                } elseif ($token->getType() === Token::NAME && $this->lexer->peek()->getType() === Token::COLON) {
226
                    break;
227
                } elseif ($token->getType() === Token::NAME && $this->lexer->peek()->getValue()[0] === '@') {
228
                    $attribute[$i] = $token->getValue();
229
                    $this->lexer->getToken();
230
                } elseif ($token->getType() === Token::NAME || $token->getType() === Token::STRING) {
231
                    if ($action) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $action of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
232
                        $g = $this->context->genNonTerminal();
233
                        $r = new Production($action, $pos);
234
                        $r->body = [$g];
235
                        $gbuffer[$i++] = $g;
236
                        $attribute[$i] = null;
237
                        $r->link = $r->body[0]->value;
238
                        $g->value = $this->context->addGram($r);
239
                    }
240
                    $gbuffer[$i++] = $w = $this->context->internSymbol($token->getValue(), false);
241
                    $attribute[$i] = null;
242
                    if ($w->isterminal) {
243
                        $lastTerm = $w;
244
                    }
245
                    $action = null;
246
                } else {
247
                    break;
248
                }
249
            }
250
            if (!$action) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $action of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
251
                if ($i > 1 && $gbuffer[0]->type !== null && $gbuffer[0]->type !== $gbuffer[1]->type) {
252
                    throw new ParseException('Stack types are different');
253
                }
254
            }
255
            $r = new Production($action, $pos);
256
257
            $r->body = \array_slice($gbuffer, 0, $i);
258
            $r->precedence = $lastTerm->precedence;
259
            $r->associativity = $lastTerm->associativity & Symbol::MASK;
260
            $r->link = $r->body[0]->value;
261
            $gbuffer[0]->value = $this->context->addGram($r);
262
263
            if ($token->getType() === Token::SEMICOLON) {
264
                $token = $this->lexer->getToken();
265
            }
266
        }
267
268
        $this->context->gram(0)->body[] = $this->context->startSymbol;
269
        $this->startPrime->value = null;
270
        foreach ($this->context->nonterminals as $key => $symbol) {
271
            if ($symbol === $this->startPrime) {
272
                continue;
273
            }
274
            if (($j = $symbol->value) === null) {
275
                throw new ParseException("Nonterminal {$symbol->name} used but not defined");
276
            }
277
            $k = null;
278
            while ($j) {
279
                $w = $j->link;
280
                $j->link = $k;
281
                $k = $j;
282
                $j = $w;
283
            }
284
            $symbol->value = $k;
285
        }
286
    }
287
288
    /**
289
     * @throws ParseException
290
     * @throws \PhpYacc\Exception\LexingException
291
     */
292
    protected function doDeclaration()
293
    {
294
        $this->eofToken = $this->context->internSymbol('EOF', true);
295
        $this->eofToken->value = 0;
296
        $this->errorToken = $this->context->internSymbol('error', true);
297
        $this->startPrime = $this->context->internSymbol("\$start", false);
298
299
        while (($token = $this->lexer->getToken())->getType() !== Token::MARK) {
300
            switch ($token->getType()) {
301
                case Token::TOKEN:
302
                case Token::RIGHT:
303
                case Token::LEFT:
304
                case Token::NONASSOC:
305
                    $this->doToken($token);
306
                    break;
307
308
                case Token::BEGININC:
309
                    $this->doCopy();
310
                    break;
311
312
                case Token::UNION:
313
                    $this->doUnion();
314
                    $this->context->unioned = true;
315
                    break;
316
317
                case Token::TYPE:
318
                    $this->doType();
319
                    break;
320
321
                case Token::EXPECT:
322
                    $token = $this->lexer->getToken();
323
                    if ($token->getType() === Token::NUMBER) {
324
                        $this->context->expected = (int) $token->getValue();
325
                    } else {
326
                        throw ParseException::unexpected($token, Token::NUMBER);
327
                    }
328
                    break;
329
330
                case Token::START:
331
                    $token = $this->lexer->getToken();
332
                    $this->context->startSymbol = $this->context->internSymbol($token->getValue(), false);
333
                    break;
334
335
                case Token::PURE_PARSER:
336
                    $this->context->pureFlag = true;
337
                    break;
338
339
                case Token::EOF:
340
                    throw new ParseException('No grammar given');
341
                default:
342
                    throw new ParseException("Syntax error, unexpected {$token->getValue()}");
343
            }
344
        }
345
346
        $base = 256;
347
        foreach ($this->context->terminals as $terminal) {
348
            if ($terminal === $this->context->eofToken) {
349
                continue;
350
            }
351
            if ($terminal->value < 0) {
352
                $terminal->value = $base++;
353
            }
354
        }
355
    }
356
357
    /**
358
     * @param Token $tag
359
     *
360
     * @throws ParseException
361
     * @throws \PhpYacc\Exception\LexingException
362
     */
363
    protected function doToken(Token $tag)
364
    {
365
        $preIncr = 0;
366
        $type = $this->getType();
367
        $token = $this->lexer->getToken();
368
369
        while ($token->getType() === Token::NAME || $token->getType() === Token::STRING) {
370
            $p = $this->context->internSymbol($token->getValue(), true);
371
372 View Code Duplication
            if ($p->name[0] === "'") {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
373
                $p->value = Utils::characterValue(\mb_substr($p->name, 1, -1));
374
            }
375
376
            if ($type) {
377
                $p->type = $type;
378
            }
379
380
            switch ($tag->getType()) {
381
                case Token::LEFT:
382
                    $p->associativity |= Symbol::LEFT;
383
                    break;
384
                case Token::RIGHT:
385
                    $p->associativity |= Symbol::RIGHT;
386
                    break;
387
                case Token::NONASSOC:
388
                    $p->associativity |= Symbol::NON;
389
                    break;
390
            }
391
392
            if ($tag->getType() !== Token::TOKEN) {
393
                $p->precedence = $this->currentPrecedence;
394
                $preIncr = 1;
395
            }
396
397
            $token = $this->lexer->getToken();
398
            if ($token->getType() === Token::NUMBER) {
399
                if ($p->value === null) {
400
                    $p->value = (int) $token->getValue();
401
                } else {
402
                    throw new ParseException(sprintf('Unexpected Token::NUMBER as %s already has a value', $p->name));
403
                }
404
                $token = $this->lexer->getToken();
405
            }
406
407
            if ($token->getType() === Token::COMMA) {
408
                $token = $this->lexer->getToken();
409
            }
410
        }
411
412
        $this->lexer->ungetToken();
413
        $this->currentPrecedence += $preIncr;
414
    }
415
416
    /**
417
     * @throws ParseException
418
     * @throws \PhpYacc\Exception\LexingException
419
     *
420
     * @return null|Symbol
421
     */
422
    protected function getType()
423
    {
424
        $token = $this->lexer->getToken();
425
426
        if ($token->getValue()[0] !== '<') {
427
            $this->lexer->ungetToken();
428
429
            return null;
430
        }
431
432
        $ct = 1;
433
        $p = '';
434
        $token = $this->lexer->getToken();
435
436
        while (true) {
437
            switch ($token->getValue()[0]) {
438
                case "\n":
439
                case "\0":
440
                    throw ParseException::unexpected($token, '>');
441
                case '<':
442
                    $ct++;
443
                    break;
444
                case '>':
445
                    $ct--;
446
                    break;
447
            }
448
449
            if ($ct === 0) {
450
                break;
451
            }
452
453
            $p .= $token->getValue();
454
            $token = $this->lexer->getRawToken();
455
        }
456
        $this->context->unioned = true;
457
458
        return $this->context->intern($p);
459
    }
460
461
    /**
462
     * @return void
463
     */
464
    protected function doCopy()
465
    {
466
        // TODO
467
    }
468
469
    protected function doUnion()
470
    {
471
        // TODO
472
    }
473
}
474