Parser::previewToken()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 3
c 0
b 0
f 0
nc 2
nop 0
dl 0
loc 6
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Remorhaz\UniLex\Parser\LL1;
6
7
use Remorhaz\UniLex\Exception;
8
use Remorhaz\UniLex\Grammar\ContextFree\GrammarInterface;
9
use Remorhaz\UniLex\Parser\LL1\Lookup\Table;
10
use Remorhaz\UniLex\Parser\Production;
11
use Remorhaz\UniLex\Parser\Symbol;
12
use Remorhaz\UniLex\Stack\SymbolStack;
13
use Remorhaz\UniLex\Stack\StackableSymbolInterface;
14
use Remorhaz\UniLex\Lexer\Token;
15
use Remorhaz\UniLex\Lexer\TokenReaderInterface;
16
use Remorhaz\UniLex\Parser\LL1\Lookup\TableInterface;
17
use Remorhaz\UniLex\Parser\LL1\Lookup\TableBuilder;
18
use Throwable;
19
20
class Parser
21
{
22
    private $grammar;
23
24
    private $lookupTable;
25
26
    private $symbolStack;
27
28
    private $token;
29
30
    private $tokenReader;
31
32
    private $listener;
33
34
    private $nextSymbolIndex = 0;
35
36
    /**
37
     * @var Production[]
38
     */
39
    private $productionMap = [];
40
41
    public function __construct(
42
        GrammarInterface $grammar,
43
        TokenReaderInterface $tokenReader,
44
        ParserListenerInterface $listener
45
    ) {
46
        $this->grammar = $grammar;
47
        $this->tokenReader = $tokenReader;
48
        $this->listener = $listener;
49
        $this->symbolStack = new SymbolStack();
50
    }
51
52
    /**
53
     * @param string $fileName
54
     * @throws Exception
55
     */
56
    public function loadLookupTable(string $fileName): void
57
    {
58
        /** @noinspection PhpIncludeInspection */
59
        $data = @include $fileName;
60
        if (false === $data) {
61
            throw new Exception("Failed to load lookup table from file {$fileName}");
62
        }
63
        $table = new Table();
64
        $table->importMap($data);
65
        $this->lookupTable = $table;
66
    }
67
68
    /**
69
     * @throws Exception
70
     * @throws UnexpectedTokenException
71
     */
72
    public function run(): void
73
    {
74
        $this->initRun();
75
        while ($this->hasSymbolInStack()) {
76
            $symbol = $this->popSymbol();
77
            if ($symbol instanceof Symbol) {
78
                $this->isTerminalSymbol($symbol)
79
                    ? $this->readSymbolToken($symbol)
80
                    : $this->pushMatchingProduction($symbol);
81
                continue;
82
            }
83
            if ($symbol instanceof Production) {
84
                $this->listener->onFinishProduction($symbol);
85
            }
86
        }
87
    }
88
89
    private function initRun(): void
90
    {
91
        $this->nextSymbolIndex = 0;
92
        $this->productionMap = [];
93
        $this->symbolStack->reset();
94
        $this->listener->onStart();
95
        $this->pushProduction($this->createRootProduction());
96
    }
97
98
    private function createRootProduction(): Production
99
    {
100
        $rootSymbol = new Symbol($this->getNextSymbolIndex(), $this->grammar->getRootSymbol());
101
        $this->listener->onRootSymbol($rootSymbol);
102
        return $this->createParsedProduction($rootSymbol, 0);
103
    }
104
105
    private function getNextSymbolIndex(): int
106
    {
107
        return $this->nextSymbolIndex++;
108
    }
109
110
    private function hasSymbolInStack(): bool
111
    {
112
        return !$this->symbolStack->isEmpty();
113
    }
114
115
    private function isTerminalSymbol(Symbol $symbol): bool
116
    {
117
        return $this->grammar->isTerminal($symbol->getSymbolId());
118
    }
119
120
    private function previewToken(): Token
121
    {
122
        if (!isset($this->token)) {
123
            $this->token = $this->tokenReader->read();
124
        }
125
        return $this->token;
126
    }
127
128
    /**
129
     * @return StackableSymbolInterface
130
     * @throws Exception
131
     */
132
    private function popSymbol(): StackableSymbolInterface
133
    {
134
        return $this->symbolStack->pop();
135
    }
136
137
    /**
138
     * @param Symbol $symbol
139
     * @throws Exception
140
     */
141
    private function onSymbol(Symbol $symbol): void
142
    {
143
        if (!isset($this->productionMap[$symbol->getIndex()])) {
144
            throw new Exception("No production in map for symbol {$symbol->getIndex()}");
145
        }
146
        [$symbolIndex, $production] = $this->productionMap[$symbol->getIndex()];
147
        $this->listener->onSymbol($symbolIndex, $production);
148
    }
149
150
    /**
151
     * @param Symbol $symbol
152
     * @throws Exception
153
     */
154
    private function readSymbolToken(Symbol $symbol): void
155
    {
156
        $token = $this->previewToken();
157
        if (!$this->grammar->tokenMatchesTerminal($symbol->getSymbolId(), $token->getType())) {
158
            throw new Exception("Unexpected token {$token->getType()} for symbol {$symbol->getSymbolId()}");
159
        }
160
        $token->isEoi()
161
            ? $this->listener->onEoi($symbol, $token)
162
            : $this->listener->onToken($symbol, $token);
163
        $this->onSymbol($symbol);
164
        unset($this->token);
165
    }
166
167
    /**
168
     * @param Symbol $symbol
169
     * @throws UnexpectedTokenException
170
     * @throws Exception
171
     */
172
    private function pushMatchingProduction(Symbol $symbol): void
173
    {
174
        $this->onSymbol($symbol);
175
        $production = $this->getMatchingProduction($symbol, $this->previewToken());
176
        $this->pushProduction($production);
177
    }
178
179
    private function pushProduction(Production $production): void
180
    {
181
        $this->symbolStack->push($production);
182
        foreach ($production->getSymbolList() as $symbolIndexInProduction => $symbol) {
183
            $this->productionMap[$symbol->getIndex()] = [$symbolIndexInProduction, $production];
184
        }
185
        $this->symbolStack->push(...$production->getSymbolList());
186
        $this->listener->onBeginProduction($production);
187
    }
188
189
    /**
190
     * @param Symbol $symbol
191
     * @param Token $token
192
     * @return Production
193
     * @throws UnexpectedTokenException
194
     * @throws Exception
195
     */
196
    private function getMatchingProduction(Symbol $symbol, Token $token): Production
197
    {
198
        $lookupTable = $this->getLookupTable();
199
        try {
200
            $productionIndex = $lookupTable->getProductionIndex($symbol->getSymbolId(), $token->getType());
201
        } catch (Throwable $e) {
202
            $expectedTokenList = $lookupTable->getExpectedTokenList($symbol->getSymbolId());
203
            $error = new UnexpectedTokenError($token, $symbol, ...$expectedTokenList);
204
            throw new UnexpectedTokenException($error, 0, $e);
205
        }
206
        return $this->createParsedProduction($symbol, $productionIndex);
207
    }
208
209
    private function createParsedProduction(Symbol $symbol, $productionIndex): Production
210
    {
211
        $grammarProduction = $this
212
            ->grammar
213
            ->getProduction($symbol->getSymbolId(), $productionIndex);
214
        $symbolList = [];
215
        foreach ($grammarProduction->getSymbolList() as $symbolId) {
216
            $symbolList[] = new Symbol($this->getNextSymbolIndex(), $symbolId);
217
        }
218
        return new Production($symbol, $grammarProduction->getIndex(), ...$symbolList);
219
    }
220
221
    /**
222
     * @return TableInterface
223
     * @throws Exception
224
     */
225
    private function getLookupTable(): TableInterface
226
    {
227
        if (!isset($this->lookupTable)) {
228
            $builder = new TableBuilder($this->grammar);
229
            $this->lookupTable = $builder->getTable();
230
        }
231
        return $this->lookupTable;
232
    }
233
}
234