1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* This file is part of Railt 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 Railt\Compiler\Grammar\PP2; |
11
|
|
|
|
12
|
|
|
use Railt\Compiler\Exception\GrammarException; |
13
|
|
|
use Railt\Compiler\Grammar\PP2\Builder\AlternationBuilder; |
14
|
|
|
use Railt\Compiler\Grammar\PP2\Builder\Builder; |
15
|
|
|
use Railt\Compiler\Grammar\PP2\Builder\ConcatenationBuilder; |
16
|
|
|
use Railt\Compiler\Grammar\PP2\Builder\TokenBuilder; |
17
|
|
|
use Railt\Compiler\Iterator\LookaheadIterator; |
18
|
|
|
use Railt\Compiler\Reader\ProvideRules; |
19
|
|
|
use Railt\Compiler\Reader\ProvideTokens; |
20
|
|
|
use Railt\Io\Readable; |
21
|
|
|
use Railt\Lexer\TokenInterface; |
22
|
|
|
use Railt\Parser\Rule\Production; |
23
|
|
|
use Railt\Parser\Rule\Symbol; |
24
|
|
|
use Railt\Parser\Rule\Terminal; |
25
|
|
|
use Railt\Parser\Rule\Token; |
26
|
|
|
|
27
|
|
|
/** |
28
|
|
|
* Class Analyzer |
29
|
|
|
*/ |
30
|
|
|
class Analyzer |
31
|
|
|
{ |
32
|
|
|
/** |
33
|
|
|
* A list of parsed rules |
34
|
|
|
* |
35
|
|
|
* @var array|Symbol[]|Terminal[]|Production[] |
36
|
|
|
*/ |
37
|
|
|
private $parsed = []; |
38
|
|
|
|
39
|
|
|
/** |
40
|
|
|
* @var array|Builder[] |
41
|
|
|
*/ |
42
|
|
|
private $builders = []; |
43
|
|
|
|
44
|
|
|
/** |
45
|
|
|
* @var Mapping |
46
|
|
|
*/ |
47
|
|
|
private $mapping; |
48
|
|
|
|
49
|
|
|
/** |
50
|
|
|
* @var array |
51
|
|
|
*/ |
52
|
|
|
private $ruleTokens = []; |
53
|
|
|
|
54
|
|
|
/** |
55
|
|
|
* @var ProvideTokens |
56
|
|
|
*/ |
57
|
|
|
private $tokens; |
58
|
|
|
|
59
|
|
|
/** |
60
|
|
|
* @var Readable |
61
|
|
|
*/ |
62
|
|
|
private $file; |
63
|
|
|
|
64
|
|
|
/** |
65
|
|
|
* @var ProvideRules |
66
|
|
|
*/ |
67
|
|
|
private $rules; |
68
|
|
|
|
69
|
|
|
/** |
70
|
|
|
* @var Builder |
71
|
|
|
*/ |
72
|
|
|
private $lastRule; |
73
|
|
|
|
74
|
|
|
/** |
75
|
|
|
* Analyzer constructor. |
76
|
|
|
* @param ProvideTokens $tokens |
77
|
|
|
* @param ProvideRules $rules |
78
|
|
|
*/ |
79
|
|
|
public function __construct(ProvideTokens $tokens, ProvideRules $rules) |
80
|
|
|
{ |
81
|
|
|
$this->mapping = new Mapping(); |
82
|
|
|
$this->tokens = $tokens; |
83
|
|
|
$this->rules = $rules; |
84
|
|
|
} |
85
|
|
|
|
86
|
|
|
/** |
87
|
|
|
* @param string $rule |
88
|
|
|
* @param iterable $tokens |
89
|
|
|
*/ |
90
|
|
|
public function add(string $rule, iterable $tokens): void |
91
|
|
|
{ |
92
|
|
|
$this->ruleTokens[$rule] = $tokens; |
93
|
|
|
} |
94
|
|
|
|
95
|
|
|
/** |
96
|
|
|
* @param string $rule |
97
|
|
|
* @return bool |
98
|
|
|
*/ |
99
|
|
|
public function isCompleted(string $rule): bool |
100
|
|
|
{ |
101
|
|
|
return \array_key_exists($rule, $this->parsed); |
102
|
|
|
} |
103
|
|
|
|
104
|
|
|
/** |
105
|
|
|
* @return array |
106
|
|
|
* @throws \InvalidArgumentException |
107
|
|
|
* @throws \Railt\Io\Exception\ExternalFileException |
108
|
|
|
*/ |
109
|
|
|
public function getResult(): array |
110
|
|
|
{ |
111
|
|
|
$this->parsed = []; |
112
|
|
|
$this->parse(); |
113
|
|
|
|
114
|
|
|
foreach ($this->builders as $builder) { |
115
|
|
|
$this->parsed[] = $builder->reduce(); |
116
|
|
|
} |
117
|
|
|
|
118
|
|
|
return $this->parsed; |
119
|
|
|
} |
120
|
|
|
|
121
|
|
|
/** |
122
|
|
|
* @throws \InvalidArgumentException |
123
|
|
|
* @throws \Railt\Io\Exception\ExternalFileException |
124
|
|
|
*/ |
125
|
|
|
private function parse(): void |
126
|
|
|
{ |
127
|
|
|
foreach ($this->ruleTokens as $name => $tokens) { |
128
|
|
|
$this->file = $this->rules->getFile($name); |
129
|
|
|
|
130
|
|
|
$iterator = new LookaheadIterator($tokens); |
131
|
|
|
$rule = $this->sequence($iterator); |
132
|
|
|
|
133
|
|
|
if ($this->rules->isKeep($name) && ! $rule->hasName()) { |
134
|
|
|
$rule->rename($name); |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
$this->store($rule); |
138
|
|
|
} |
139
|
|
|
} |
140
|
|
|
|
141
|
|
|
/** |
142
|
|
|
* @param Builder $builder |
143
|
|
|
* @return Builder |
144
|
|
|
*/ |
145
|
|
|
private function store(Builder $builder): Builder |
146
|
|
|
{ |
147
|
|
|
return $this->builders[] = $this->lastRule = $builder; |
148
|
|
|
} |
149
|
|
|
|
150
|
|
|
/** |
151
|
|
|
* @param LookaheadIterator $tokens |
152
|
|
|
* @return Builder |
153
|
|
|
* @throws \InvalidArgumentException |
154
|
|
|
* @throws \Railt\Io\Exception\ExternalFileException |
155
|
|
|
*/ |
156
|
|
|
private function choice(LookaheadIterator $tokens): Builder |
157
|
|
|
{ |
158
|
|
|
$choice = new AlternationBuilder($this->mapping); |
159
|
|
|
$choice->addChildBuilder($this->lastRule); |
160
|
|
|
$tokens->next(); |
161
|
|
|
|
162
|
|
|
while ($tokens->valid()) { |
163
|
|
|
$child = $this->terminal($tokens); |
164
|
|
|
|
165
|
|
|
if ($child) { |
166
|
|
|
$choice->addChildBuilder($this->store($child)); |
167
|
|
|
} |
168
|
|
|
|
169
|
|
|
$continue = $tokens->getNext() && $tokens->getNext()->name() === Lexer::T_OR; |
170
|
|
|
|
171
|
|
|
if (! $continue) { |
172
|
|
|
break; |
173
|
|
|
} |
174
|
|
|
|
175
|
|
|
$tokens->next(); |
176
|
|
|
$tokens->next(); |
177
|
|
|
} |
178
|
|
|
|
179
|
|
|
return $choice; |
180
|
|
|
} |
181
|
|
|
|
182
|
|
|
/** |
183
|
|
|
* @return Mapping |
184
|
|
|
*/ |
185
|
|
|
public function getMapping(): Mapping |
186
|
|
|
{ |
187
|
|
|
return $this->mapping; |
188
|
|
|
} |
189
|
|
|
|
190
|
|
|
/** |
191
|
|
|
* @param LookaheadIterator $tokens |
192
|
|
|
* @return Builder |
193
|
|
|
* @throws \InvalidArgumentException |
194
|
|
|
* @throws \Railt\Io\Exception\ExternalFileException |
195
|
|
|
*/ |
196
|
|
|
private function sequence(LookaheadIterator $tokens): Builder |
197
|
|
|
{ |
198
|
|
|
$children = []; |
199
|
|
|
|
200
|
|
|
while ($tokens->valid()) { |
201
|
|
|
$child = $this->terminal($tokens); |
202
|
|
|
|
203
|
|
|
if ($child) { |
204
|
|
|
$children[] = $this->store($child); |
205
|
|
|
} |
206
|
|
|
|
207
|
|
|
$tokens->next(); |
208
|
|
|
} |
209
|
|
|
|
210
|
|
|
if (\count($children) > 1) { |
211
|
|
|
$sequence = new ConcatenationBuilder($this->mapping); |
212
|
|
|
$sequence->addChildrenBuilders($children); |
213
|
|
|
|
214
|
|
|
return $sequence; |
215
|
|
|
} |
216
|
|
|
|
217
|
|
|
return \reset($children); |
218
|
|
|
} |
219
|
|
|
|
220
|
|
|
private function repeat(LookaheadIterator $tokens): Builder |
|
|
|
|
221
|
|
|
{ |
222
|
|
|
|
223
|
|
|
} |
224
|
|
|
|
225
|
|
|
/** |
226
|
|
|
* @param LookaheadIterator $tokens |
227
|
|
|
* @return Builder |
228
|
|
|
* @throws \InvalidArgumentException |
229
|
|
|
* @throws \Railt\Io\Exception\ExternalFileException |
230
|
|
|
*/ |
231
|
|
|
private function group(LookaheadIterator $tokens): Builder |
232
|
|
|
{ |
233
|
|
|
$children = []; |
234
|
|
|
|
235
|
|
|
$tokens->next(); |
236
|
|
|
while ($tokens->valid() && $tokens->current()->name() !== Lexer::T_GROUP_CLOSE) { |
237
|
|
|
$children[] = $tokens->current(); |
238
|
|
|
$tokens->next(); |
239
|
|
|
} |
240
|
|
|
|
241
|
|
|
return $this->sequence(new LookaheadIterator($children)); |
|
|
|
|
242
|
|
|
} |
243
|
|
|
|
244
|
|
|
/** |
245
|
|
|
* @param LookaheadIterator $tokens |
246
|
|
|
* @return null|Builder |
247
|
|
|
* @throws \InvalidArgumentException |
248
|
|
|
* @throws \Railt\Io\Exception\ExternalFileException |
249
|
|
|
*/ |
250
|
|
|
private function terminal(LookaheadIterator $tokens): ?Builder |
251
|
|
|
{ |
252
|
|
|
/** @var TokenInterface $current */ |
253
|
|
|
$current = $tokens->current(); |
254
|
|
|
|
255
|
|
|
switch ($current->name()) { |
256
|
|
|
case Lexer::T_OR: |
|
|
|
|
257
|
|
|
return $this->choice($tokens); |
258
|
|
|
|
259
|
|
|
case Lexer::T_ZERO_OR_ONE: |
260
|
|
|
case Lexer::T_ONE_OR_MORE: |
261
|
|
|
case Lexer::T_ZERO_OR_MORE: |
262
|
|
|
case Lexer::T_N_TO_M: |
263
|
|
|
case Lexer::T_ZERO_TO_M: |
264
|
|
|
case Lexer::T_N_OR_MORE: |
265
|
|
|
case Lexer::T_EXACTLY_N: |
266
|
|
|
return $this->repeat($tokens); |
267
|
|
|
|
268
|
|
|
case Lexer::T_KEPT: |
269
|
|
|
return $this->token($current, true); |
270
|
|
|
|
271
|
|
|
case Lexer::T_SKIPPED: |
272
|
|
|
return $this->token($current, false); |
273
|
|
|
|
274
|
|
|
case Lexer::T_INVOKE: |
275
|
|
|
return $this->invoke($current); |
276
|
|
|
|
277
|
|
|
case Lexer::T_GROUP_OPEN: |
278
|
|
|
return $this->group($tokens); |
279
|
|
|
|
280
|
|
|
case Lexer::T_RENAME: |
281
|
|
|
$this->lastRule->rename($current->value(1)); |
282
|
|
|
return null; |
283
|
|
|
} |
284
|
|
|
|
285
|
|
|
throw (new GrammarException(\sprintf('Unrecognized terminal %s', $current))) |
286
|
|
|
->throwsIn($this->file, $current->offset()); |
287
|
|
|
} |
288
|
|
|
|
289
|
|
|
/** |
290
|
|
|
* @param TokenInterface $token |
291
|
|
|
* @param bool $keep |
292
|
|
|
* @return TokenBuilder |
293
|
|
|
* @throws \Railt\Io\Exception\ExternalFileException |
294
|
|
|
*/ |
295
|
|
|
private function token(TokenInterface $token, bool $keep): TokenBuilder |
296
|
|
|
{ |
297
|
|
|
$name = $token->value(1); |
298
|
|
|
|
299
|
|
|
if (! $this->tokens->has($name)) { |
300
|
|
|
$error = \sprintf('Token "%s" is not defined', $name); |
301
|
|
|
throw (new GrammarException($error)) |
302
|
|
|
->throwsIn($this->file, $token->offset()); |
303
|
|
|
} |
304
|
|
|
|
305
|
|
|
return new TokenBuilder($this->mapping, $name, $keep); |
306
|
|
|
} |
307
|
|
|
|
308
|
|
|
/** |
309
|
|
|
* @param TokenInterface $invocation |
310
|
|
|
* @return Builder |
311
|
|
|
* @throws \Railt\Io\Exception\ExternalFileException |
312
|
|
|
*/ |
313
|
|
|
private function invoke(TokenInterface $invocation): Builder |
314
|
|
|
{ |
315
|
|
|
$name = $invocation->value(1); |
316
|
|
|
|
317
|
|
|
if (! $this->rules->has($name)) { |
318
|
|
|
$error = \sprintf('Rule "%s" is not defined', $name); |
319
|
|
|
throw (new GrammarException($error)) |
320
|
|
|
->throwsIn($this->file, $invocation->offset()); |
321
|
|
|
} |
322
|
|
|
|
323
|
|
|
// TODO |
324
|
|
|
} |
325
|
|
|
} |
326
|
|
|
|
This check looks from parameters that have been defined for a function or method, but which are not used in the method body.