Passed
Push — master ( 7b55bb...b913d7 )
by Edward
03:52
created

TokenMatcherGenerator::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 2
b 0
f 0
nc 1
nop 1
dl 0
loc 3
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Remorhaz\UniLex\Lexer;
6
7
use ReflectionException;
8
use Remorhaz\UniLex\AST\Translator;
9
use Remorhaz\UniLex\AST\Tree;
10
use Remorhaz\UniLex\Exception;
11
use Remorhaz\UniLex\RegExp\FSM\Dfa;
12
use Remorhaz\UniLex\RegExp\FSM\DfaBuilder;
13
use Remorhaz\UniLex\RegExp\FSM\Nfa;
14
use Remorhaz\UniLex\RegExp\FSM\NfaBuilder;
15
use Remorhaz\UniLex\RegExp\FSM\Range;
16
use Remorhaz\UniLex\RegExp\FSM\RangeSet;
17
use Remorhaz\UniLex\RegExp\FSM\TransitionMap;
18
use Remorhaz\UniLex\RegExp\ParserFactory;
19
use Remorhaz\UniLex\RegExp\PropertyLoader;
20
use Remorhaz\UniLex\Unicode\CharBufferFactory;
21
use Throwable;
22
23
use function array_diff;
24
use function array_intersect;
25
use function array_keys;
26
use function array_merge;
27
use function array_pop;
28
use function array_unique;
29
use function count;
30
use function implode;
31
use function in_array;
32
use function var_export;
33
34
class TokenMatcherGenerator
35
{
36
37
    private $spec;
38
39
    private $output;
40
41
    private $dfa;
42
43
    private $regExpMap = [];
44
45
    private $visitedHashMap = [];
46
47
    public function __construct(TokenMatcherSpec $spec)
48
    {
49
        $this->spec = $spec;
50
    }
51
52
    /**
53
     * @return string
54
     * @throws Exception
55
     * @throws ReflectionException
56
     */
57
    private function buildOutput(): string
58
    {
59
        return
60
            "{$this->buildFileComment()}\ndeclare(strict_types=1);\n\n" .
61
            "{$this->buildHeader()}\n" .
62
            "class {$this->spec->getTargetShortName()} extends {$this->spec->getTemplateClass()->getShortName()}\n" .
63
            "{\n" .
64
            "\n" .
65
            "    public function match({$this->buildMatchParameters()}): bool\n" .
66
            "    {\n{$this->buildMatchBody()}" .
67
            "    }\n" .
68
            "}\n";
69
    }
70
71
    /**
72
     * @return TokenMatcherInterface
73
     * @throws Exception
74
     */
75
    public function load(): TokenMatcherInterface
76
    {
77
        $targetClass = $this->spec->getTargetClassName();
78
        if (!class_exists($targetClass)) {
79
            try {
80
                $source = $this->getOutput(false);
81
                eval($source);
0 ignored issues
show
introduced by
The use of eval() is discouraged.
Loading history...
82
            } catch (Throwable $e) {
83
                throw new Exception("Invalid PHP code generated", 0, $e);
84
            }
85
            if (!class_exists($targetClass)) {
86
                throw new Exception("Failed to generate target class");
87
            }
88
        }
89
90
        return new $targetClass();
91
    }
92
93
    /**
94
     * @param bool $asFile
95
     * @return string
96
     * @throws Exception
97
     * @throws ReflectionException
98
     */
99
    public function getOutput(bool $asFile = true): string
100
    {
101
        if (!isset($this->output)) {
102
            $this->output = $this->buildOutput();
103
        }
104
105
        return $asFile ? "<?php\n\n{$this->output}" : $this->output;
106
    }
107
108
    private function buildFileComment(): string
109
    {
110
        $content = $this->spec->getFileComment();
111
        if ('' == $content) {
112
            return '';
113
        }
114
        $comment = "/**\n";
115
        $commentLineList = explode("\n", $content);
116
        foreach ($commentLineList as $commentLine) {
117
            $comment .= rtrim(" * {$commentLine}") . "\n";
118
        }
119
        $comment .= " */\n";
120
121
        return $comment;
122
    }
123
124
    /**
125
     * @return string
126
     * @throws ReflectionException
127
     */
128
    public function buildHeader(): string
129
    {
130
        $headerParts = [];
131
        $namespace = $this->spec->getTargetNamespaceName();
132
        if ($namespace != '') {
133
            $headerParts[] = $this->buildMethodPart("namespace {$namespace};", 0);
134
        }
135
        $useList = $this->buildUseList();
136
        if ('' != $useList) {
137
            $headerParts[] = $useList;
138
        }
139
        $header = $this->buildMethodPart($this->spec->getHeader(), 0);
140
        if ('' != $header) {
141
            $headerParts[] = $header;
142
        }
143
144
        return implode("\n", $headerParts);
145
    }
146
147
    /**
148
     * @return string
149
     * @throws ReflectionException
150
     */
151
    private function buildUseList(): string
152
    {
153
        $result = '';
154
        foreach ($this->spec->getUsedClassList() as $alias => $className) {
155
            $classWithAlias = is_string($alias) ? "{$className} {$alias}" : $className;
156
            $result .= $this->buildMethodPart("use {$classWithAlias};", 0);
157
        }
158
159
        return $result;
160
    }
161
162
    /**
163
     * @return string
164
     * @throws ReflectionException
165
     */
166
    private function buildMatchParameters(): string
167
    {
168
        $paramList = [];
169
        foreach ($this->spec->getMatchMethod()->getParameters() as $matchParameter) {
170
            if ($matchParameter->hasType()) {
171
                $param = $matchParameter->getType()->isBuiltin()
172
                    ? $matchParameter->getType()->getName()
173
                    : $matchParameter->getClass()->getShortName();
174
                $param .= " ";
175
            } else {
176
                $param = "";
177
            }
178
            $param .= "\${$matchParameter->getName()}";
179
            $paramList[] = $param;
180
        }
181
182
        return implode(", ", $paramList);
183
    }
184
185
    /**
186
     * @return string
187
     * @throws Exception
188
     */
189
    private function buildMatchBody(): string
190
    {
191
        $result = $this->buildBeforeMatch();
192
193
        foreach ($this->spec->getModeList() as $mode) {
194
            if (TokenMatcherInterface::DEFAULT_MODE == $mode) {
195
                continue;
196
            }
197
            $result .=
198
                $this->buildMethodPart("if (\$context->getMode() == '{$mode}') {") .
199
                $this->buildFsmEntry($mode, 3) .
200
                $this->buildMethodPart("}");
201
        }
202
        foreach ($this->spec->getModeList() as $mode) {
203
            if (TokenMatcherInterface::DEFAULT_MODE == $mode) {
204
                $result .= $this->buildFsmEntry(TokenMatcherInterface::DEFAULT_MODE) . "\n";
205
            }
206
            $result .= $this->buildFsmMoves($mode);
207
        }
208
209
        $result .= $this->buildErrorState();
210
211
        return $result;
212
    }
213
214
    private function buildBeforeMatch(): string
215
    {
216
        return
217
            $this->buildMethodPart("\$context = \$this->createContext(\$buffer, \$tokenFactory);") .
218
            $this->buildMethodPart($this->spec->getBeforeMatch());
219
    }
220
221
    /**
222
     * @param string $mode
223
     * @param int    $indent
224
     * @return string
225
     * @throws Exception
226
     */
227
    private function buildFsmEntry(string $mode, int $indent = 2): string
228
    {
229
        $state = $this->getDfa($mode)->getStateMap()->getStartState();
230
231
        return $this->buildMethodPart("goto {$this->buildStateLabel('state', $mode, $state)};", $indent);
232
    }
233
234
    private function buildStateLabel(string $prefix, string $mode, int $state): string
235
    {
236
        $contextSuffix = TokenMatcherInterface::DEFAULT_MODE == $mode
237
            ? ''
238
            : ucfirst($mode);
239
240
        return "{$prefix}{$contextSuffix}{$state}";
241
    }
242
243
    /**
244
     * @param string $mode
245
     * @return string
246
     * @throws Exception
247
     */
248
    private function buildFsmMoves(string $mode): string
249
    {
250
        $result = '';
251
        foreach ($this->getDfa($mode)->getStateMap()->getStateList() as $stateIn) {
252
            if ($this->isFinishStateWithSingleEnteringTransition($mode, $stateIn)) {
253
                continue;
254
            }
255
            $result .=
256
                $this->buildStateEntry($mode, $stateIn) .
257
                $this->buildStateTransitionList($mode, $stateIn) .
258
                $this->buildStateFinish($mode, $stateIn);
259
        }
260
261
        return $result;
262
    }
263
264
    /**
265
     * @param string $mode
266
     * @param int    $stateIn
267
     * @return string
268
     * @throws Exception
269
     */
270
    private function buildStateEntry(string $mode, int $stateIn): string
271
    {
272
        $result = '';
273
        $result .= $this->buildMethodPart("{$this->buildStateLabel('state', $mode, $stateIn)}:");
274
        $moves = $this->getDfa($mode)->getTransitionMap()->getExitList($stateIn);
275
        if (empty($moves)) {
276
            return $result;
277
        }
278
        $result .= $this->buildMethodPart("if (\$context->getBuffer()->isEnd()) {");
279
        $result .= $this->getDfa($mode)->getStateMap()->isFinishState($stateIn)
280
            ? $this->buildMethodPart("goto {$this->buildStateLabel('finish', $mode, $stateIn)};", 3)
281
            : $this->buildMethodPart("goto error;", 3);
282
        $result .=
283
            $this->buildMethodPart("}") .
284
            $this->buildMethodPart("\$char = \$context->getBuffer()->getSymbol();");
285
286
        return $result;
287
    }
288
289
    /**
290
     * @param string $mode
291
     * @param int    $stateIn
292
     * @return string
293
     * @throws Exception
294
     */
295
    private function buildStateTransitionList(string $mode, int $stateIn): string
296
    {
297
        $result = '';
298
        foreach ($this->getDfa($mode)->getTransitionMap()->getExitList($stateIn) as $stateOut => $symbolList) {
299
            foreach ($symbolList as $symbol) {
300
                $rangeSet = $this->getDfa($mode)->getSymbolTable()->getRangeSet($symbol);
301
                $result .=
302
                    $this->buildMethodPart("if ({$this->buildRangeSetCondition($rangeSet)}) {") .
303
                    $this->buildOnTransition() .
304
                    $this->buildMethodPart("\$context->getBuffer()->nextSymbol();", 3) .
305
                    $this->buildMarkTransitionVisited($mode, $stateIn, $stateOut, $symbol, 3);
306
                $result .= $this->isFinishStateWithSingleEnteringTransition($mode, $stateOut)
307
                    ? $this->buildToken($mode, $stateOut, 3)
308
                    : $this->buildStateTransition($mode, $stateOut, 3);
309
                $result .= $this->buildMethodPart("}");
310
            }
311
        }
312
313
        return $result;
314
    }
315
316
    private function buildMarkTransitionVisited(
317
        string $mode,
318
        int $stateIn,
319
        int $stateOut,
320
        int $symbol,
321
        int $indent = 3
322
    ): string {
323
        $result = '';
324
        $hash = $this->buildHash($stateIn, $stateOut, $symbol);
325
        foreach ($this->visitedHashMap[$mode] ?? [] as $regExps) {
326
            foreach ($regExps as $hashes) {
327
                if (in_array($hash, $hashes)) {
328
                    $hashArg = var_export($hash, true);
329
                    $result .= $this->buildMethodPart("\$context->visitTransition({$hashArg});", $indent);
330
                    break 2;
331
                }
332
            }
333
        }
334
335
        return $result;
336
    }
337
338
    /**
339
     * @param string $mode
340
     * @param int    $stateOut
341
     * @param int    $indent
342
     * @return string
343
     */
344
    private function buildStateTransition(string $mode, int $stateOut, int $indent = 3): string
345
    {
346
        return $this->buildMethodPart("goto {$this->buildStateLabel('state', $mode, $stateOut)};", $indent);
347
    }
348
349
    /**
350
     * @param string $mode
351
     * @param int    $stateOut
352
     * @return bool
353
     * @throws Exception
354
     */
355
    private function isFinishStateWithSingleEnteringTransition(string $mode, int $stateOut): bool
356
    {
357
        if (!$this->getDfa($mode)->getStateMap()->isFinishState($stateOut)) {
358
            return false;
359
        }
360
        $enters = $this->getDfa($mode)->getTransitionMap()->getEnterList($stateOut);
361
        $exits = $this->getDfa($mode)->getTransitionMap()->getExitList($stateOut);
362
        if (!(count($enters) == 1 && count($exits) == 0)) {
363
            return false;
364
        }
365
        $symbolList = array_pop($enters);
366
367
        return count($symbolList) == 1;
368
    }
369
370
    private function buildHex(int $char): string
371
    {
372
        $hexChar = strtoupper(dechex($char));
373
        if (strlen($hexChar) % 2 != 0) {
374
            $hexChar = "0{$hexChar}";
375
        }
376
377
        return "0x{$hexChar}";
378
    }
379
380
    private function buildRangeCondition(Range $range): array
381
    {
382
        $startChar = $this->buildHex($range->getStart());
383
        if ($range->getStart() == $range->getFinish()) {
384
            return ["{$startChar} == \$char"];
385
        }
386
        $finishChar = $this->buildHex($range->getFinish());
387
        if ($range->getStart() + 1 == $range->getFinish()) {
388
            return [
389
                "{$startChar} == \$char",
390
                "{$finishChar} == \$char",
391
            ];
392
        }
393
394
        return ["{$startChar} <= \$char && \$char <= {$finishChar}"];
395
    }
396
397
    private function buildRangeSetCondition(RangeSet $rangeSet): string
398
    {
399
        $conditionList = [];
400
        foreach ($rangeSet->getRanges() as $range) {
401
            $conditionList = array_merge($conditionList, $this->buildRangeCondition($range));
402
        }
403
        $result = implode(" || ", $conditionList);
404
        if (strlen($result) + 15 <= 120 || count($conditionList) == 1) {
405
            return ltrim($result);
406
        }
407
        $result = $this->buildMethodPart(implode(" ||\n", $conditionList), 1);
408
409
        return "\n    " . ltrim($result);
410
    }
411
412
    /**
413
     * @param string $mode
414
     * @param int    $stateIn
415
     * @return string
416
     * @throws Exception
417
     */
418
    private function buildStateFinish(string $mode, int $stateIn): string
419
    {
420
        if (!$this->getDfa($mode)->getStateMap()->isFinishState($stateIn)) {
421
            return $this->buildMethodPart("goto error;\n");
422
        }
423
        $result = '';
424
        if (!empty($this->getDfa($mode)->getTransitionMap()->getExitList($stateIn))) {
425
            $result .= $this->buildMethodPart("{$this->buildStateLabel('finish', $mode, $stateIn)}:");
426
        }
427
        $result .= "{$this->buildToken($mode, $stateIn)}\n";
428
429
        return $result;
430
    }
431
432
    /**
433
     * @param string $mode
434
     * @param int    $stateIn
435
     * @param int    $indent
436
     * @return string
437
     * @throws Exception
438
     */
439
    private function buildToken(string $mode, int $stateIn, int $indent = 2): string
440
    {
441
        $result = '';
442
        $regExpCount = 0;
443
        $defaultRegExp = null;
444
        foreach ($this->visitedHashMap[$mode][$stateIn] ?? [] as $regExp => $visitedHashes) {
445
            if (empty($visitedHashes)) {
446
                $defaultRegExp = (string) $regExp;
447
                $regExpCount++;
448
                continue;
449
            }
450
            $visitedHashValues = [];
451
            foreach ($visitedHashes as $visitedHash) {
452
                $visitedHashValues[] = var_export($visitedHash, true);
453
            }
454
            $visitedHashArgs = implode(', ', $visitedHashValues);
455
            $tokenSpec = $this->spec->getTokenSpec($mode, (string) $regExp);
456
            $result .=
457
                $this->buildMethodPart("if (\$context->isVisitedTransition({$visitedHashArgs})) {", $indent) .
458
                $this->buildMethodPart("// {$regExp}", $indent + 1) .
459
                $this->buildSingleToken($tokenSpec, $indent + 1) .
460
                $this->buildMethodPart("}", $indent);
461
            $regExpCount++;
462
        }
463
464
        if (0 == $regExpCount) {
465
            throw new Exception("No tokens found for state {$stateIn}");
466
        }
467
468
        if (isset($defaultRegExp)) {
469
            $tokenSpec = $this->spec->getTokenSpec($mode, $defaultRegExp);
470
471
            return
472
                $result .
473
                $this->buildMethodPart("// {$defaultRegExp}", $indent) .
474
                $this->buildSingleToken($tokenSpec, $indent);
475
        }
476
477
        return
478
            $result .
479
            $this->buildMethodPart("goto error;", $indent);
480
    }
481
482
    private function buildSingleToken(TokenSpec $tokenSpec, int $indent): string
483
    {
484
        return
485
            $this->buildMethodPart($tokenSpec->getCode(), $indent) .
486
            $this->buildOnToken($indent) . "\n" .
487
            $this->buildMethodPart("return true;", $indent);
488
    }
489
490
    private function buildErrorState(): string
491
    {
492
        $code = $this->spec->getOnError();
493
494
        return
495
            $this->buildMethodPart("error:") .
496
            $this->buildMethodPart('' == $code ? "return false;" : $code);
497
    }
498
499
    private function buildMethodPart(string $code, int $indent = 2): string
500
    {
501
        if ('' == $code) {
502
            return '';
503
        }
504
        $result = '';
505
        $codeLineList = explode("\n", $code);
506
        foreach ($codeLineList as $codeLine) {
507
            $line = '';
508
            for ($i = 0; $i < $indent; $i++) {
509
                $line .= "    ";
510
            }
511
            $result .= rtrim($line . $codeLine) . "\n";
512
        }
513
514
        return $result;
515
    }
516
517
    private function buildOnTransition(): string
518
    {
519
        return $this->buildMethodPart($this->spec->getOnTransition(), 3);
520
    }
521
522
    private function buildOnToken(int $indent = 2): string
523
    {
524
        return $this->buildMethodPart($this->spec->getOnToken(), $indent);
525
    }
526
527
    /**
528
     * @param string $context
529
     * @return Dfa
530
     * @throws Exception
531
     */
532
    private function getDfa(string $context): Dfa
533
    {
534
        if (!isset($this->dfa[$context])) {
535
            $this->dfa[$context] = $this->buildDfa($context);
536
        }
537
538
        return $this->dfa[$context];
539
    }
540
541
    /**
542
     * @param string $mode
543
     * @return Dfa
544
     * @throws Exception
545
     */
546
    private function buildDfa(string $mode): Dfa
547
    {
548
        $nfa = new Nfa();
549
        $startState = $nfa->getStateMap()->createState();
550
        $nfa->getStateMap()->addStartState($startState);
551
        $nfaRegExpMap = [];
552
        foreach ($this->spec->getTokenSpecList($mode) as $tokenSpec) {
553
            $existingStates = $nfa->getStateMap()->getStateList();
554
            $regExpEntryState = $nfa->getStateMap()->createState();
555
            $nfa
556
                ->getEpsilonTransitionMap()
557
                ->addTransition($startState, $regExpEntryState, true);
558
            $this->buildRegExp($nfa, $regExpEntryState, $tokenSpec->getRegExp());
559
            $nfaRegExpMap[$tokenSpec->getRegExp()] = array_diff(
560
                $nfa->getStateMap()->getStateList(),
561
                $existingStates
562
            );
563
        }
564
565
        $dfa = DfaBuilder::fromNfa($nfa);
566
        $dfaRegExpMap = [];
567
        foreach (array_keys($nfaRegExpMap, null, true) as $regExp) {
568
            $dfaRegExpMap[$regExp] = [];
569
        }
570
        $allDfaStateIds = $dfa->getStateMap()->getStateList();
571
        foreach ($allDfaStateIds as $dfaStateId) {
572
            $nfaStateIds = $dfa->getStateMap()->getStateValue($dfaStateId);
573
            foreach ($nfaRegExpMap as $regExp => $nfaRegExpStateIds) {
574
                if (!empty(array_intersect($nfaStateIds, $nfaRegExpStateIds))) {
575
                    $dfaRegExpMap[(string) $regExp][] = $dfaStateId; // TODO: why the hell integer?..
576
                }
577
            }
578
        }
579
        $this->regExpMap[$mode] = [];
580
        foreach ($dfaRegExpMap as $regExp => $regExpStateIds) {
581
            $this->regExpMap[$mode][(string) $regExp] = [$regExpStateIds, array_diff($allDfaStateIds, $regExpStateIds)];
582
        }
583
        $nfaRegExpTransitionMap = new TransitionMap($nfa->getStateMap());
584
        foreach ($nfa->getSymbolTransitionMap()->getTransitionList() as $nfaSourceStateId => $nfaTransitionTargets) {
585
            foreach ($nfaTransitionTargets as $nfaTargetStateId => $nfaSymbolIds) {
586
                $regExps = [];
587
                foreach ($nfaRegExpMap as $regExp => $nfaRegExpIds) {
588
                    if (in_array($nfaSourceStateId, $nfaRegExpIds) || in_array($nfaTargetStateId, $nfaRegExpIds)) {
589
                        $regExps[] = (string) $regExp;
590
                    }
591
                }
592
                $nfaTransitionValue = [];
593
                foreach ($nfaSymbolIds as $nfaSymbolId) {
594
                    $nfaTransitionValue[$nfaSymbolId] = $regExps;
595
                }
596
                $nfaRegExpTransitionMap->addTransition($nfaSourceStateId, $nfaTargetStateId, $nfaTransitionValue);
597
            }
598
        }
599
600
        $map = [];
601
        $mapIn = [];
602
        $mapOut = [];
603
        foreach ($dfa->getTransitionMap()->getTransitionList() as $dfaSourceStateId => $dfaTransitionTargets) {
604
            foreach ($dfaTransitionTargets as $dfaTargetStateId => $dfaSymbolIds) {
605
                $matchingNfaSourceStateIds = $dfa->getStateMap()->getStateValue($dfaSourceStateId);
606
                $matchingNfaTargetStateIds = $dfa->getStateMap()->getStateValue($dfaTargetStateId);
607
                $dfaTransitionValue = [];
608
                foreach ($matchingNfaSourceStateIds as $nfaSourceStateId) {
609
                    foreach ($matchingNfaTargetStateIds as $nfaTargetStateId) {
610
                        if (
611
                            $nfa->getStateMap()->stateExists($nfaSourceStateId) && // TODO: find out invalid id
612
                            $nfa->getStateMap()->stateExists($nfaTargetStateId) &&
613
                            $nfaRegExpTransitionMap->transitionExists($nfaSourceStateId, $nfaTargetStateId)
614
                        ) {
615
                            $nfaTransitionValue = $nfaRegExpTransitionMap->getTransition(
616
                                $nfaSourceStateId,
617
                                $nfaTargetStateId
618
                            );
619
                            foreach ($dfaSymbolIds as $dfaSymbolId) {
620
                                if (isset($nfaTransitionValue[$dfaSymbolId])) {
621
                                    $dfaTransitionValue[$dfaSymbolId] = array_unique(
622
                                        array_merge(
623
                                            $dfaTransitionValue[$dfaSymbolId] ?? [],
624
                                            $nfaTransitionValue[$dfaSymbolId]
625
                                        )
626
                                    );
627
                                }
628
                            }
629
                        }
630
                    }
631
                }
632
                foreach ($dfaTransitionValue as $dfaSymbolId => $regExps) {
633
                    $hash = $this->buildHash($dfaSourceStateId, $dfaTargetStateId, $dfaSymbolId);
634
                    $map[$hash] = $regExps;
635
                    $mapIn[$dfaSourceStateId] = array_merge($mapIn[$dfaSourceStateId] ?? [], [$hash]);
636
                    $mapOut[$dfaTargetStateId] = array_merge($mapOut[$dfaTargetStateId] ?? [], [$hash]);
637
                }
638
            }
639
        }
640
        $incomingTransitionsForHash = [];
641
        $incomingTransitionsForState = [];
642
        foreach ($mapIn as $stateId => $hashes) {
643
            foreach ($hashes as $hash) {
644
                $incomingTransitionsForHash[$hash] = $mapOut[$stateId] ?? [];
645
            }
646
        }
647
        foreach (array_keys($mapOut) as $stateId) {
648
            if ($dfa->getStateMap()->isFinishState($stateId)) {
649
                $incomingTransitionsForState[$stateId] = $mapOut[$stateId] ?? [];
650
            }
651
        }
652
        $this->visitedHashMap[$mode] = [];
653
        foreach ($incomingTransitionsForState as $stateId => $hashes) {
654
            $inHashBuffer = $hashes;
655
            $processedHashes = [];
656
            $visitedHashes = [];
657
            $regExps = [];
658
            foreach ($hashes as $hash) {
659
                $regExps = array_unique(array_merge($regExps, $map[$hash]));
660
            }
661
            while (!empty($inHashBuffer)) {
662
                $inHash = array_pop($inHashBuffer);
663
                if (isset($processedHashes[$inHash])) {
664
                    continue;
665
                }
666
                $processedHashes[$inHash] = true;
667
                $inRegExps = array_intersect($map[$inHash], $regExps);
668
                if (count($inRegExps) == 1) {
669
                    $inRegExp = $inRegExps[array_key_first($inRegExps)];
670
                    $visitedHashes[$inRegExp][] = $inHash;
671
                    continue;
672
                }
673
                array_push($inHashBuffer, ...($incomingTransitionsForHash[$inHash] ?? []));
674
            }
675
            $this->visitedHashMap[$mode][$stateId] = $visitedHashes;
676
        }
677
        foreach ($this->visitedHashMap[$mode] as $stateId => $visitedHashes) {
678
            if (!empty($visitedHashes)) {
679
                continue;
680
            }
681
            $regExps = [];
682
            foreach ($incomingTransitionsForState[$stateId] ?? [] as $hash) {
683
                $regExps = array_unique(array_merge($map[$hash], $regExps));
684
            }
685
            $otherRegExps = [];
686
            foreach ($this->visitedHashMap[$mode] as $anotherStateId => $otherVisitedHashes) {
687
                if ($stateId == $anotherStateId) {
688
                    continue;
689
                }
690
                foreach ($otherVisitedHashes as $otherRegExp => $otherHashes) {
691
                    $otherRegExps = array_unique(array_merge($otherRegExps, [$otherRegExp]));
692
                }
693
            }
694
            $regExps = array_diff($regExps, $otherRegExps);
695
            if (count($regExps) == 1) {
696
                $regExp = $regExps[array_key_first($regExps)];
697
                $this->visitedHashMap[$mode][$stateId][$regExp] = [];
698
            }
699
        }
700
701
        return $dfa;
702
    }
703
704
    private function buildHash(int $stateIn, int $stateOut, int $symbol): string
705
    {
706
        return "{$stateIn}-{$stateOut}:{$symbol}";
707
    }
708
709
    /**
710
     * @param Nfa    $nfa
711
     * @param int    $entryState
712
     * @param string $regExp
713
     * @throws Exception
714
     */
715
    private function buildRegExp(Nfa $nfa, int $entryState, string $regExp): void
716
    {
717
        $buffer = CharBufferFactory::createFromString($regExp);
718
        $tree = new Tree();
719
        ParserFactory::createFromBuffer($tree, $buffer)->run();
720
        $nfaBuilder = new NfaBuilder($nfa, PropertyLoader::create());
721
        $nfaBuilder->setStartState($entryState);
722
        (new Translator($tree, $nfaBuilder))->run();
723
    }
724
}
725