Generator::computeKernels()   D
last analyzed

Complexity

Conditions 16
Paths 37

Size

Total Lines 79

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 79
rs 4.9115
c 0
b 0
f 0
cc 16
nc 37
nop 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\Lalr;
11
12
use PhpYacc\Exception\LogicException;
13
use PhpYacc\Grammar\Context;
14
use PhpYacc\Grammar\State;
15
use PhpYacc\Grammar\Symbol;
16
use PhpYacc\Lalr\Conflict\ReduceReduce;
17
use PhpYacc\Lalr\Conflict\ShiftReduce;
18
use PhpYacc\Support\Utils;
19
use PhpYacc\Yacc\Production;
20
21
/**
22
 * Class Generator.
23
 */
24
class Generator
25
{
26
    const NON_ASSOC = -32768;
27
28
    /**
29
     * @var Context
30
     */
31
    protected $context;
32
33
    protected $nullable;
34
    protected $blank;
35
36
    /**
37
     * @var array|State[][]
38
     */
39
    protected $statesThrough = [];
40
41
    /**
42
     * @var array
43
     */
44
    protected $visited = [];
45
46
    /**
47
     * @var BitSet[]
48
     */
49
    protected $first;
50
51
    /**
52
     * @var BitSet[]
53
     */
54
    protected $follow;
55
56
    /**
57
     * @var State[]
58
     */
59
    protected $states;
60
61
    /**
62
     * @var int
63
     */
64
    protected $countLooks;
65
66
    /**
67
     * @var int
68
     */
69
    protected $countStates;
70
71
    /**
72
     * @var int
73
     */
74
    protected $countActions;
75
76
    /**
77
     * @var int
78
     */
79
    protected $countActions2;
80
81
    /**
82
     * @var int
83
     */
84
    protected $countNonLeafStates;
85
    protected $nsrerr;
86
    protected $nrrerr;
87
88
    /**
89
     * @param Context $context
90
     *
91
     * @throws LogicException
92
     *
93
     * @return void
94
     */
95
    public function compute(Context $context)
96
    {
97
        $this->context = $context;
98
        // Ensure nil symbol is part of nSymbols
99
        $this->context->nilSymbol();
100
        $this->context->finish();
101
        $countSymbols = $this->context->countSymbols;
102
        $this->nullable = \array_fill(0, $countSymbols, false);
103
104
        $this->blank = new ArrayBitSet($countSymbols);
105
        $this->states = [];
106
        $this->countLooks = $this->countStates = $this->countActions = $this->countActions2 = 0;
107
        $this->countNonLeafStates = 0;
108
        $this->nsrerr = $this->nrrerr = 0;
109
110
        $this->statesThrough = [];
111
        $this->first = [];
112
        $this->follow = [];
113
114
        foreach ($this->context->symbols as $s) {
115
            $this->first[$s->code] = clone $this->blank;
116
            $this->follow[$s->code] = clone $this->blank;
117
            $this->statesThrough[$s->code] = [];
118
        }
119
120
        $this->computeEmpty();
121
        $this->firstNullablePrecomp();
122
        $this->computeKernels();
123
        $this->computeLookaheads();
124
        $this->fillReduce();
125
        $this->printDiagnostics();
126
        $this->printStatistics();
127
128
        $this->context->states = $this->states;
129
        $this->context->countNonLeafStates = $this->countNonLeafStates;
130
    }
131
132
    /**
133
     * @return void
134
     */
135
    protected function computeKernels()
136
    {
137
        $tmpList = new Lr1(null, clone $this->blank, new Item($this->context->gram(0), 1));
138
        $this->findOrCreateState($this->context->nilsymbol, $tmpList);
139
140
        // foreach by ref so that new additions to $this->states are also picked up
141
        foreach ($this->states as &$p) {
142
            // Collect direct GOTO's (come from kernel items)
143
144
            /** @var Lr1|null $tmpList */
145
            /** @var Lr1|null $tmpTail */
146
            $tmpList = $tmpTail = null;
147
148
            /** @var Lr1 $x */
149
            for ($x = $p->items; $x !== null; $x = $x->next) {
150
                if (!$x->isTailItem()) {
151
                    $wp = new Lr1(null, clone $this->blank, $x->item->slice(1));
152
                    if ($tmpTail !== null) {
153
                        $tmpTail->next = $wp;
154
                    } else {
155
                        $tmpList = $wp;
156
                    }
157
                    $tmpTail = $wp;
158
                }
159
            }
160
161
            // Collect indirect GOTO's (come from nonkernel items)
162
            $this->clearVisited();
163
            for ($tp = $tmpList; $tp != null; $tp = $tp->next) {
164
                /** @var Symbol $g */
165
                $g = $tp->item[-1];
166
                if ($g !== null && !$g->isTerminal && !$this->visited[$g->code]) {
167
                    $this->visited[$g->code] = true;
168
                    /** @var Production $gram */
169
                    for ($gram = $g->value; $gram != null; $gram = $gram->link) {
170
                        if (isset($gram->body[1])) {
171
                            $wp = new Lr1($g, clone $this->blank, new Item($gram, 2));
172
                            $tmpTail->next = $wp;
173
                            $tmpTail = $wp;
174
                        }
175
                    }
176
                }
177
            }
178
179
            $tmpList = $this->sortList($tmpList, function (Lr1 $x, Lr1 $y) {
180
                $gx = $x->item[-1]->code;
181
                $gy = $y->item[-1]->code;
182
                if ($gx !== $gy) {
183
                    return $gx - $gy;
184
                }
185
                $px = $x->item->getProduction();
186
                $py = $y->item->getProduction();
187
                if ($px !== $py) {
188
                    return $px->num - $py->num;
189
                }
190
191
                return $x->item->getPos() - $y->item->getPos();
192
            });
193
194
            // Compute next states
195
            $nextStates = [];
196
            for ($tp = $tmpList; $tp !== null;) {
197
                $sp = null;
198
199
                $g = $tp->item[-1];
200
                $sublist = $tp;
201
                while ($tp != null && $tp->item[-1] === $g) {
202
                    $sp = $tp;
203
                    $tp = $tp->next;
204
                }
205
                $sp->next = null;
206
207
                $nextStates[] = $this->findOrCreateState($g, $sublist);
208
            }
209
210
            $p->shifts = $nextStates;
211
            $this->countActions += \count($nextStates);
212
        }
213
    }
214
215
    /**
216
     * @return void
217
     */
218
    protected function computeLookaheads()
219
    {
220
        $this->states[0]->items->look->setBit(0);
221
        do {
222
            $changed = false;
223
            foreach ($this->states as $p) {
224
                $this->computeFollow($p);
225
                for ($x = $p->items; $x !== null; $x = $x->next) {
226
                    $g = $x->item[0] ?? null;
227
                    if (null !== $g) {
228
                        $s = $x->item->slice(1);
229
                        $t = null;
230
                        foreach ($p->shifts as $t) {
231
                            if ($t->through === $g) {
232
                                break;
233
                            }
234
                        }
235
                        \assert($t->through === $g);
236
                        for ($y = $t->items; $y !== null; $y = $y->next) {
237
                            if ($y->item == $s) {
238
                                break;
239
                            }
240
                        }
241
                        \assert($y->item == $s);
242
                        $changed |= $y->look->or($x->look);
243
                    }
244
                }
245
246
                foreach ($p->shifts as $t) {
247
                    for ($x = $t->items; $x !== null; $x = $x->next) {
248 View Code Duplication
                        if ($x->left !== null) {
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...
249
                            $changed |= $x->look->or($this->follow[$x->left->code]);
250
                        }
251
                    }
252
                }
253
254 View Code Duplication
                for ($x = $p->items; $x !== null; $x = $x->next) {
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...
255
                    if ($x->isTailItem() && $x->isHeadItem()) {
256
                        $x->look->or($this->follow[$x->item[-1]->code]);
0 ignored issues
show
Documentation introduced by
$this->follow[$x->item[-1]->code] is of type object<PhpYacc\Lalr\BitSet>, but the function expects a object<self>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
257
                    }
258
                }
259
            }
260
        } while ($changed);
261
262
        if ($this->context->debug) {
263
            foreach ($this->states as $p) {
264
                $this->context->debug("state unknown:\n");
265
                for ($x = $p->items; $x != null; $x = $x->next) {
266
                    $this->context->debug("\t".\trim((string) $x->item)."\n");
267
                    $this->context->debug("\t\t[ ");
268
                    $this->context->debug(Utils::dumpSet($this->context, $x->look));
269
                    $this->context->debug("]\n");
270
                }
271
            }
272
        }
273
    }
274
275
    /**
276
     * @throws LogicException
277
     *
278
     * @return void
279
     */
280
    protected function fillReduce()
281
    {
282
        $this->clearVisited();
283
284
        foreach ($this->states as $p) {
285
            /** @var Reduce[] $tmpr */
286
            $tmpr = [];
287
288
            $tdefact = 0;
289
            foreach ($p->shifts as $t) {
290
                if ($t->through === $this->context->errorToken) {
291
                    // shifting error
292
                    $tdefact = -1;
293
                }
294
            }
295
296
            // Pick up reduce entries
297
            for ($x = $p->items; $x !== null; $x = $x->next) {
298
                if (!$x->isTailItem()) {
299
                    continue;
300
                }
301
302
                $alook = clone $x->look;
303
                $gram = $x->item->getProduction();
304
305
                // find shift/reduce conflict
306
                foreach ($p->shifts as $m => $t) {
307
                    $e = $t->through;
308
                    if (!$e->isTerminal) {
309
                        break;
310
                    }
311
                    if ($alook->testBit($e->code)) {
312
                        $rel = $this->comparePrecedence($gram, $e);
313
                        if ($rel === self::NON_ASSOC) {
314
                            $alook->clearBit($e->code);
315
                            unset($p->shifts[$m]);
316
                            $tmpr[] = new Reduce($e, -1);
317
                        } elseif ($rel < 0) {
318
                            // reduce
319
                            unset($p->shifts[$m]);
320
                        } elseif ($rel > 0) {
321
                            // shift
322
                            $alook->clearBit($e->code);
323
                        } elseif ($rel == 0) {
324
                            // conflict
325
                            $alook->clearBit($e->code);
326
                            $this->nsrerr++;
327
                            $p->conflict = new Conflict\ShiftReduce($t, $gram->num, $e, $p->conflict);
328
                        }
329
                    }
330
                }
331
332
                foreach ($tmpr as $reduce) {
333
                    if ($alook->testBit($reduce->symbol->code)) {
334
                        // reduce/reduce conflict
335
                        $this->nrrerr++;
336
                        $p->conflict = new Conflict\ReduceReduce(
337
                            $reduce->number,
338
                            $gram->num,
339
                            $reduce->symbol,
340
                            $p->conflict
341
                        );
342
343
                        if ($gram->num < $reduce->number) {
344
                            $reduce->number = $gram->num;
345
                        }
346
                        $alook->clearBit($reduce->symbol->code);
347
                    }
348
                }
349
350
                foreach ($alook as $e) {
351
                    $sym = $this->context->symbols[$e];
352
                    $tmpr[] = new Reduce($sym, $gram->num);
353
                }
354
            }
355
356
            // Decide default action
357
            if (!$tdefact) {
358
                $tdefact = -1;
359
360 View Code Duplication
                Utils::stableSort($tmpr, function (Reduce $x, Reduce $y) {
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...
361
                    if ($x->number != $y->number) {
362
                        return $y->number - $x->number;
363
                    }
364
365
                    return $x->symbol->code - $y->symbol->code;
366
                });
367
368
                $maxn = 0;
369
                $nr = \count($tmpr);
370
                for ($j = 0; $j < $nr;) {
371
                    for ($k = $j; $j < $nr; $j++) {
372
                        if ($tmpr[$j]->number != $tmpr[$k]->number) {
373
                            break;
374
                        }
375
                    }
376
                    if ($j - $k > $maxn && $tmpr[$k]->number > 0) {
377
                        $maxn = $j - $k;
378
                        $tdefact = $tmpr[$k]->number;
379
                    }
380
                }
381
            }
382
383
            // Squeeze tmpr
384
            $tmpr = \array_filter($tmpr, function (Reduce $reduce) use ($tdefact) {
385
                return $reduce->number !== $tdefact;
386
            });
387
388 View Code Duplication
            Utils::stableSort($tmpr, function (Reduce $x, Reduce $y) {
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...
389
                if ($x->symbol !== $y->symbol) {
390
                    return $x->symbol->code - $y->symbol->code;
391
                }
392
393
                return $x->number - $y->number;
394
            });
395
            $tmpr[] = new Reduce($this->context->nilsymbol, $tdefact);
396
397
            // Squeeze shift actions (we deleted some keys)
398
            $p->shifts = \array_values($p->shifts);
399
400
            foreach ($tmpr as $reduce) {
401
                if ($reduce->number >= 0) {
402
                    $this->visited[$reduce->number] = true;
403
                }
404
            }
405
406
            // Register tmpr
407
            $p->reduce = $tmpr;
408
            $this->countActions2 += \count($tmpr);
409
        }
410
411
        $k = 0;
412
        foreach ($this->context->grams as $gram) {
413
            if (!$this->visited[$gram->num]) {
414
                $k++;
415
                $this->context->debug("Never reduced: \n"); // TODO
416
            }
417
        }
418
419
        if ($k) {
420
            $this->context->debug($k." rule(s) never reduced\n");
421
        }
422
423
        // Sort states in decreasing order of entries
424
        // do not move initial state
425
        $initState = \array_shift($this->states);
426
427
        Utils::stableSort($this->states, function (State $p, State $q) {
428
            $numReduces = count($p->reduce) - 1; // -1 for default action
429
            $pt = $numReduces;
430
            $pn = count($p->shifts) + $numReduces;
431
            foreach ($p->shifts as $x) {
432
                if ($x->through->isTerminal) {
433
                    $pt++;
434
                }
435
            }
436
437
            $numReduces = \count($q->reduce) - 1; // -1 for default action
438
            $qt = $numReduces;
439
            $qn = \count($q->shifts) + $numReduces;
440
            foreach ($q->shifts as $x) {
441
                if ($x->through->isTerminal) {
442
                    $qt++;
443
                }
444
            }
445
446
            if ($pt !== $qt) {
447
                return $qt - $pt;
448
            }
449
450
            return $qn - $pn;
451
        });
452
453
        array_unshift($this->states, $initState);
454
455
        foreach ($this->states as $i => $p) {
456
            $p->number = $i;
457
            if (!empty($p->shifts) || !$p->reduce[0]->symbol->isNilSymbol()) {
458
                $this->countNonLeafStates = $i + 1;
0 ignored issues
show
Documentation Bug introduced by
It seems like $i + 1 can also be of type double. However, the property $countNonLeafStates is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
459
            }
460
        }
461
462
        foreach ($this->states as $state) {
463
            $this->printState($state);
464
        }
465
    }
466
467
    /**
468
     * @param Production $gram
469
     * @param Symbol     $x
470
     *
471
     * @throws LogicException
472
     *
473
     * @return int|mixed
474
     */
475
    protected function comparePrecedence(Production $gram, Symbol $x)
476
    {
477
        if ($gram->associativity === Symbol::UNDEF
478
            || ($x->associativity & Symbol::MASK) === Symbol::UNDEF
479
        ) {
480
            return 0;
481
        }
482
483
        $v = $x->precedence - $gram->precedence;
484
        if ($v !== 0) {
485
            return $v;
486
        }
487
488
        switch ($gram->associativity) {
489
            case Symbol::LEFT:
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
490
                return -1;
491
            case Symbol::RIGHT:
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
492
                return 1;
493
            case Symbol::NON:
494
                return self::NON_ASSOC;
495
        }
496
497
        throw new LogicException('Gram has associativity other than LEFT/RIGHT/NON. This should never happen');
498
    }
499
500
    /**
501
     * @param State $st
502
     */
503
    protected function computeFollow(State $st)
504
    {
505
        foreach ($st->shifts as $t) {
506
            if (!$t->through->isTerminal) {
507
                $this->follow[$t->through->code] = clone $this->blank;
508 View Code Duplication
                for ($x = $t->items; $x !== null && !$x->isHeadItem(); $x = $x->next) {
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...
509
                    $this->computeFirst($this->follow[$t->through->code], $x->item);
510
                }
511
            }
512
        }
513
        for ($x = $st->items; $x !== null; $x = $x->next) {
514
            /** @var Symbol $g */
515
            $g = $x->item[0] ?? null;
516
            if ($g !== null && !$g->isTerminal && $this->isSeqNullable($x->item->slice(1))) {
517
                $this->follow[$g->code]->or($x->look);
0 ignored issues
show
Documentation introduced by
$x->look is of type object<PhpYacc\Lalr\BitSet>, but the function expects a object<self>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
518
            }
519
        }
520
        do {
521
            $changed = false;
522
            foreach ($st->shifts as $t) {
523
                if (!$t->through->isTerminal) {
524
                    $p = $this->follow[$t->through->code];
525
                    for ($x = $t->items; $x !== null && !$x->isHeadItem(); $x = $x->next) {
526 View Code Duplication
                        if ($this->isSeqNullable($x->item) && $x->left != null) {
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...
527
                            $changed |= $p->or($this->follow[$x->left->code]);
528
                        }
529
                    }
530
                }
531
            }
532
        } while ($changed);
533
    }
534
535
    /**
536
     * @param BitSet $p
537
     * @param Item   $item
538
     */
539
    protected function computeFirst(BitSet $p, Item $item)
540
    {
541
        /** @var Symbol $g */
542
        foreach ($item as $g) {
543
            if ($g->isTerminal) {
544
                $p->setBit($g->code);
545
546
                return;
547
            }
548
            $p->or($this->first[$g->code]);
0 ignored issues
show
Documentation introduced by
$this->first[$g->code] is of type object<PhpYacc\Lalr\BitSet>, but the function expects a object<self>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
549
            if (!$this->nullable[$g->code]) {
550
                return;
551
            }
552
        }
553
    }
554
555
    /**
556
     * @param Item $item
557
     *
558
     * @return bool
559
     */
560
    protected function isSeqNullable(Item $item)
561
    {
562
        /** @var Symbol $g */
563
        foreach ($item as $g) {
564
            if ($g->isTerminal || !$this->nullable[$g->code]) {
565
                return false;
566
            }
567
        }
568
569
        return true;
570
    }
571
572
    /**
573
     * @param Symbol $through
574
     * @param Lr1    $sublist
575
     *
576
     * @return State
577
     */
578
    protected function findOrCreateState(Symbol $through, Lr1 $sublist)
579
    {
580
        foreach ($this->statesThrough[$through->code] as $state) {
581
            if (Utils::isSameSet($state->items, $sublist)) {
582
                return $state;
583
            }
584
        }
585
586
        $state = new State($through, $this->makeState($sublist));
587
        $this->states[] = $state;
588
        $this->statesThrough[$through->code][] = $state;
589
        $this->countStates++;
590
591
        return $state;
592
    }
593
594
    /**
595
     * @return void
596
     */
597
    protected function computeEmpty()
598
    {
599
        do {
600
            $changed = false;
601
            foreach ($this->context->grams as $gram) {
602
603
                /** @var Symbol $left */
604
                $left = $gram->body[0];
605
                $right = $gram->body[1] ?? null;
606
                if (($right === null || ($right->associativity & Production::EMPTY)) && !($left->associativity & Production::EMPTY)) {
607
                    $left->setAssociativityFlag(Production::EMPTY);
608
                    $changed = true;
609
                }
610
            }
611
        } while ($changed);
612
613
        if ($this->context->debug) {
614
            $this->context->debug('EMPTY nonterminals: ');
615
            foreach ($this->context->nonterminals as $symbol) {
616
                if ($symbol->associativity & Production::EMPTY) {
617
                    $this->context->debug(' '.$symbol->name);
618
                }
619
            }
620
            $this->context->debug("\n");
621
        }
622
    }
623
624
    /**
625
     * @return void
626
     */
627
    protected function firstNullablePrecomp()
628
    {
629
        do {
630
            $changed = false;
631
            foreach ($this->context->grams as $gram) {
632
                $h = $gram->body[0];
633
                for ($s = 1, $l = \count($gram->body); $s < $l; $s++) {
634
                    $g = $gram->body[$s];
635
                    if ($g->isTerminal) {
636
                        if (!$this->first[$h->code]->testBit($g->code)) {
637
                            $changed = true;
638
                            $this->first[$h->code]->setBit($g->code);
639
                        }
640
                        continue 2;
641
                    }
642
643
                    $changed |= $this->first[$h->code]->or($this->first[$g->code]);
644
                    if (!$this->nullable[$g->code]) {
645
                        continue 2;
646
                    }
647
                }
648
649
                if (!$this->nullable[$h->code]) {
650
                    $this->nullable[$h->code] = true;
651
                    $changed = true;
652
                }
653
            }
654
        } while ($changed);
655
656
        if ($this->context->debug) {
657
            $this->context->debug("First:\n");
658
            foreach ($this->context->nonterminals as $symbol) {
659
                $this->context->debug("{$symbol->name}\t[ ");
660
                $this->context->debug(Utils::dumpSet($this->context, $this->first[$symbol->code]));
661
                if ($this->nullable[$symbol->code]) {
662
                    $this->context->debug('@ ');
663
                }
664
                $this->context->debug("]\n");
665
            }
666
        }
667
    }
668
669
    /**
670
     * @param Lr1 $items
671
     *
672
     * @return Lr1
673
     */
674
    protected function makeState(Lr1 $items): Lr1
675
    {
676
        $tail = null;
677
        for ($p = $items; $p !== null; $p = $p->next) {
678
            $p->look = null;
679
            if ($p->left !== null) {
680
                for ($q = $items; $q !== $p; $q = $q->next) {
681
                    if ($q->left === $p->left) {
682
                        $p->look = $q->look;
683
                        break;
684
                    }
685
                }
686
            }
687
            if ($p->look === null) {
688
                $p->look = clone $this->blank;
689
                $this->countLooks++;
690
            }
691
            $tail = $p;
692
        }
693
        $this->clearVisited();
694
        for ($p = $items; $p !== null; $p = $p->next) {
695
            /** @var Symbol $g */
696
            $g = $p->item[0] ?? null;
697
            if ($g !== null && !$g->isTerminal) {
698
                $tail = $this->findEmpty($tail, $g);
0 ignored issues
show
Bug introduced by
It seems like $tail can be null; however, findEmpty() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
699
            }
700
        }
701
702
        return $items;
703
    }
704
705
    /**
706
     * @return void
707
     */
708
    protected function clearVisited()
709
    {
710
        $nSymbols = $this->context->countSymbols;
711
        $nGrams = $this->context->countGrams;
712
        $this->visited = array_fill(0, max($nSymbols, $nGrams), false);
713
    }
714
715
    /**
716
     * @param Lr1    $tail
717
     * @param Symbol $x
718
     *
719
     * @return Lr1
720
     */
721
    protected function findEmpty(Lr1 $tail, Symbol $x): Lr1
722
    {
723
        if (!$this->visited[$x->code] && ($x->associativity & Production::EMPTY)) {
724
            $this->visited[$x->code] = true;
725
726
            /** @var Production $gram */
727
            for ($gram = $x->value; $gram !== null; $gram = $gram->link) {
728
                if ($gram->isEmpty()) {
729
                    $p = new Lr1(null, clone $this->blank, new Item($gram, 1));
730
                    $tail->next = $p;
731
                    $tail = $p;
732
                    $this->countLooks++;
733
                } elseif (!$gram->body[1]->isTerminal) {
734
                    $tail = $this->findEmpty($tail, $gram->body[1]);
735
                }
736
            }
737
        }
738
739
        return $tail;
740
    }
741
742
    /**
743
     * @param Lr1|null $list
744
     * @param callable $cmp
745
     *
746
     * @return mixed|null|Lr1
747
     */
748
    protected function sortList(Lr1 $list = null, callable $cmp)
749
    {
750
        $array = [];
751
        for ($x = $list; $x !== null; $x = $x->next) {
752
            $array[] = $x;
753
        }
754
755
        Utils::stableSort($array, $cmp);
756
757
        $list = null;
758
        /** @var Lr1 $tail */
759
        $tail = null;
760
        foreach ($array as $x) {
761
            if ($list == null) {
762
                $list = $x;
763
            } else {
764
                $tail->next = $x;
765
            }
766
            $tail = $x;
767
            $x->next = null;
768
        }
769
770
        return $list;
771
    }
772
773
    /**
774
     * @param State $state
775
     */
776
    protected function printState(State $state)
777
    {
778
        $this->context->debug('state '.$state->number."\n");
779
        for ($conf = $state->conflict; $conf !== null; $conf = $conf->next()) {
780
            if ($conf instanceof ShiftReduce) {
781
                $this->context->debug(sprintf(
782
                    "%d: shift/reduce conflict (shift %d, reduce %d) on %s\n",
783
                    $state->number,
784
                    $conf->state()->number,
785
                    $conf->reduce(),
786
                    $conf->symbol()->name
787
                ));
788
            } elseif ($conf instanceof ReduceReduce) {
789
                $this->context->debug(sprintf(
790
                    "%d: reduce/reduce conflict (reduce %d, reduce %d) on %s\n",
791
                    $state->number,
792
                    $conf->reduce1(),
793
                    $conf->reduce2(),
794
                    $conf->symbol()->name
795
                ));
796
            }
797
        }
798
799
        for ($x = $state->items; $x !== null; $x = $x->next) {
800
            $this->context->debug("\t".\trim((string) $x->item)."\n");
801
        }
802
        $this->context->debug("\n");
803
804
        $i = $j = 0;
805
        while (true) {
806
            $s = $state->shifts[$i] ?? null;
807
            $r = $state->reduce[$j] ?? null;
808
            if ($s === null && $r === null) {
809
                break;
810
            }
811
812
            if ($s !== null && ($r === null || $s->through->code < $r->symbol->code)) {
813
                $str = $s->through->name;
814
                $this->context->debug(strlen($str) < 8 ? "\t$str\t\t" : "\t$str\t");
815
                $this->context->debug($s->through->isTerminal ? 'shift' : 'goto');
816
                $this->context->debug(' '.$s->number);
817
                if ($s->isReduceOnly()) {
818
                    $this->context->debug(' and reduce ('.$s->reduce[0]->number.')');
819
                }
820
                $this->context->debug("\n");
821
                $i++;
822
            } else {
823
                $str = $r->symbol->isNilSymbol() ? '.' : $r->symbol->name;
824
                $this->context->debug(strlen($str) < 8 ? "\t$str\t\t" : "\t$str\t");
825
                if ($r->number === 0) {
826
                    $this->context->debug("accept\n");
827
                } elseif ($r->number < 0) {
828
                    $this->context->debug("error\n");
829
                } else {
830
                    $this->context->debug("reduce ($r->number)\n");
831
                }
832
                $j++;
833
            }
834
        }
835
        $this->context->debug("\n");
836
    }
837
838
    /**
839
     * @return void
840
     */
841
    protected function printDiagnostics()
842
    {
843
        // TODO check expected_srconf
844
        $expected_srconf = 0;
845
        if ($this->nsrerr !== $expected_srconf || $this->nrrerr !== 0) {
846
            $this->context->debug("{$this->context->filename}: there are ");
847
            if ($this->nsrerr !== $expected_srconf) {
848
                $this->context->debug(" $this->nsrerr shift/reduce");
849
                if ($this->nrrerr !== 0) {
850
                    $this->context->debug(' and');
851
                }
852
            }
853
            if ($this->nrrerr !== 0) {
854
                $this->context->debug(" $this->nrrerr reduce/reduce");
855
            }
856
            $this->context->debug(" conflicts\n");
857
        }
858
    }
859
860
    /**
861
     * @return void
862
     */
863
    protected function printStatistics()
864
    {
865
        if (!$this->context->debug) {
866
            return;
867
        }
868
869
        $nterms = iterator_count($this->context->terminals);
870
        $nnonts = iterator_count($this->context->nonterminals);
871
872
        $nprods = $this->context->countGrams;
873
        $totalActs = $this->countActions + $this->countActions2;
874
875
        $this->context->debug("\nStatistics for {$this->context->filename}:\n");
876
        $this->context->debug("\t$nterms terminal symbols\n");
877
        $this->context->debug("\t$nnonts nonterminal symbols\n");
878
        $this->context->debug("\t$nprods productions\n");
879
        $this->context->debug("\t$this->countStates states\n");
880
        $this->context->debug("\t$this->countNonLeafStates non leaf states\n");
881
        $this->context->debug("\t$this->nsrerr shift/reduce, $this->nrrerr reduce/reduce conflicts\n");
882
        // items?
883
        $this->context->debug("\t$this->countLooks lookahead sets used\n");
884
        $this->context->debug("\t$this->countActions+$this->countActions2=$totalActs action entries\n");
885
        // bytes used?
886
    }
887
}
888