Completed
Push — master ( 85fc11...2d6c85 )
by Kirill
03:30
created

Runtime::parse()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 38
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 6.2488

Importance

Changes 0
Metric Value
cc 6
eloc 22
nc 5
nop 1
dl 0
loc 38
ccs 17
cts 21
cp 0.8095
crap 6.2488
rs 8.439
c 0
b 0
f 0
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\Parser;
11
12
use Railt\Compiler\Exception\ParserException;
13
use Railt\Compiler\Exception\UnexpectedTokenException;
14
use Railt\Compiler\Iterator\Buffer;
15
use Railt\Compiler\Iterator\BufferInterface;
16
use Railt\Compiler\Lexer\Result\Unknown;
17
use Railt\Compiler\LexerInterface;
18
use Railt\Compiler\Parser\Ast\Leaf;
19
use Railt\Compiler\Parser\Ast\Node;
20
use Railt\Compiler\Parser\Ast\NodeInterface;
21
use Railt\Compiler\Parser\Ast\Rule as AstRule;
22
use Railt\Compiler\Parser\Rule\Alternation;
23
use Railt\Compiler\Parser\Rule\Concatenation;
24
use Railt\Compiler\Parser\Rule\Repetition;
25
use Railt\Compiler\Parser\Rule\Symbol;
26
use Railt\Compiler\Parser\Rule\Token;
27
use Railt\Compiler\Parser\Trace\Entry;
28
use Railt\Compiler\Parser\Trace\Escape;
29
use Railt\Compiler\Parser\Trace\Invocation;
30
use Railt\Compiler\Parser\Trace\Terminator;
31
use Railt\Compiler\ParserInterface;
32
use Railt\Compiler\TokenInterface;
33
use Railt\Io\Readable;
34
35
/**
36
 * Class Parser
37
 */
38
abstract class Runtime implements ParserInterface
39
{
40
    /**
41
     * Trace of activated rules.
42
     *
43
     * @var array
44
     */
45
    protected $trace = [];
46
47
    /**
48
     * Stack of todo list.
49
     *
50
     * @var array
51
     */
52
    protected $todo;
53
54
    /**
55
     * Current depth while building the trace.
56
     *
57
     * @var int
58
     */
59
    protected $depth = -1;
60
61
    /**
62
     * @var LexerInterface
63
     */
64
    private $lexer;
65
66
    /**
67
     * @var array
68
     */
69
    private $rules;
70
71
    /**
72
     * Construct the parser.
73
     *
74
     * @param LexerInterface $lexer
75
     * @param array $rules Rules.
76
     */
77 54
    public function __construct(LexerInterface $lexer, array $rules = [])
78
    {
79 54
        $this->rules = $rules;
80 54
        $this->lexer = $lexer;
81 54
    }
82
83
    /**
84
     * Parse :-).
85
     *
86
     * @param Readable $input
87
     * @return NodeInterface
88
     * @throws ParserException
89
     * @throws \RuntimeException
90
     */
91 54
    public function parse(Readable $input): NodeInterface
92
    {
93 54
        $buffer = new Buffer($this->lex($input), 1024);
94 54
        $buffer->rewind();
95
96 54
        $this->trace = [];
97 54
        $this->todo  = [];
98
99 54
        $rule = $this->getRootRule();
100
101 54
        $closeRule  = new Escape($rule, 0);
102 54
        $openRule   = new Entry($rule, 0, [$closeRule]);
103 54
        $this->todo = [$closeRule, $openRule];
104
105
        do {
106 54
            $out = $this->unfold($buffer);
107
108 54
            if ($out !== null && $buffer->current()->name() === TokenInterface::END_OF_INPUT) {
109 54
                break;
110
            }
111
112 33
            if ($this->backtrack($buffer) === false) {
113
                /** @var TokenInterface $token */
114
                $token = $buffer->top();
115
116
                $error = \sprintf('Unregistered token %s', $token);
117
                throw UnexpectedTokenException::fromFile($error, $input, $token->offset());
118
            }
119 33
        } while (true);
120
121 54
        $ast = $this->buildTree();
122
123 54
        if (! ($ast instanceof NodeInterface)) {
124
            throw new ParserException('Parsing error: cannot build AST, the trace is corrupted.', 1);
125
        }
126
127 54
        return $ast;
128
    }
129
130
    /**
131
     * @param Readable $input
132
     * @return \Traversable
133
     */
134 54
    private function lex(Readable $input): \Traversable
135
    {
136 54
        foreach ($this->getLexer()->lex($input) as $token) {
137 54
            if ($token instanceof Unknown) {
138
                $error = \sprintf('Unexpected lexical token %s', $token);
139
                throw UnexpectedTokenException::fromFile($error, $input, $token->offset());
140
            }
141
142 54
            yield $token;
143
        }
144 54
    }
145
146
    /**
147
     * @return LexerInterface
148
     */
149 54
    public function getLexer(): LexerInterface
150
    {
151 54
        return $this->lexer;
152
    }
153
154
    /**
155
     * Get root rule.
156
     *
157
     * @return string
158
     */
159 54
    public function getRootRule(): string
160
    {
161 54
        foreach ($this->rules as $rule => $_) {
162 54
            if (\is_string($rule)) {
163 54
                return $rule;
164
            }
165
        }
166
167
        throw new \RuntimeException('Invalid grammar root rule (Can not find)');
168
    }
169
170
    /**
171
     * Unfold trace
172
     * @param BufferInterface $buffer
173
     * @return mixed
174
     */
175 54
    protected function unfold(BufferInterface $buffer)
176
    {
177 54
        while (0 < \count($this->todo)) {
178 54
            $rule = \array_pop($this->todo);
179
180 54
            if ($rule instanceof Escape) {
181 54
                $rule->setDepth($this->depth);
182 54
                $this->trace($rule, $buffer);
0 ignored issues
show
Compatibility introduced by
$buffer of type object<Railt\Compiler\Iterator\BufferInterface> is not a sub-type of object<Railt\Compiler\Iterator\Buffer>. It seems like you assume a concrete implementation of the interface Railt\Compiler\Iterator\BufferInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
183
184 54
                if ($rule->isTransitional() === false) {
185 54
                    --$this->depth;
186
                }
187
            } else {
188 54
                $out = $this->parseCurrentRule($buffer, $this->getRule($rule->getRule()), $rule->getData());
0 ignored issues
show
Bug introduced by
It seems like $this->getRule($rule->getRule()) can be null; however, parseCurrentRule() 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...
189
190 54
                if ($out === false && $this->backtrack($buffer) === false) {
191
                    return;
192
                }
193
            }
194
        }
195
196 54
        return true;
197
    }
198
199
    /**
200
     * @param Invocation $invocation
201
     * @param Buffer $buffer
202
     */
203 54
    private function trace(Invocation $invocation, Buffer $buffer): void
204
    {
205 54
        $this->trace[] = $invocation;
206 54
        $invocation->at($buffer->current()->offset());
207 54
    }
208
209
    /**
210
     * Parse current rule
211
     * @param BufferInterface $buffer
212
     * @param Symbol $rule Current rule.
213
     * @param int $next Next rule index.
214
     * @return bool
215
     */
216 54
    protected function parseCurrentRule(BufferInterface $buffer, Symbol $rule, $next): bool
217
    {
218 54
        if ($rule instanceof Token) {
219 54
            $name = $buffer->current()->name();
220
221 54
            if ($rule->getName() !== $name) {
222 26
                return false;
223
            }
224
225 54
            $value = $buffer->current()->value();
226
227 54
            $current = $buffer->current();
228
229 54
            $offset = $current->offset();
230
231 54
            \array_pop($this->todo);
232 54
            $this->trace[] = new Terminator($rule->getName(), $value, $offset, $rule->isKept());
233 54
            $buffer->next();
234
235 54
            return true;
236
        }
237
238 54
        if ($rule instanceof Concatenation) {
239 54
            $this->trace(new Entry($rule->getId(), 0, null, $this->depth), $buffer);
0 ignored issues
show
Compatibility introduced by
$buffer of type object<Railt\Compiler\Iterator\BufferInterface> is not a sub-type of object<Railt\Compiler\Iterator\Buffer>. It seems like you assume a concrete implementation of the interface Railt\Compiler\Iterator\BufferInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
240 54
            $children = $rule->then();
241
242 54
            for ($i = \count($children) - 1; $i >= 0; --$i) {
243 54
                $nextRule     = $children[$i];
244 54
                $this->todo[] = new Escape($nextRule, 0);
245 54
                $this->todo[] = new Entry($nextRule, 0);
246
            }
247
248 54
            return true;
249
        }
250
251 50
        if ($rule instanceof Alternation) {
252 33
            $children = $rule->then();
253
254 33
            if ($next >= \count($children)) {
255 3
                return false;
256
            }
257
258 33
            $this->trace(new Entry($rule->getId(), $next, $this->todo, $this->depth), $buffer);
0 ignored issues
show
Compatibility introduced by
$buffer of type object<Railt\Compiler\Iterator\BufferInterface> is not a sub-type of object<Railt\Compiler\Iterator\Buffer>. It seems like you assume a concrete implementation of the interface Railt\Compiler\Iterator\BufferInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
259
            /** @var string|int $nextRule */
260 33
            $nextRule     = $children[$next];
261 33
            $this->todo[] = new Escape($nextRule, 0);
262 33
            $this->todo[] = new Entry($nextRule, 0);
263
264 33
            return true;
265
        }
266
267 40
        if ($rule instanceof Repetition) {
268 40
            $nextRule = $rule->then()[0];
269
270 40
            if ($next === 0) {
271 40
                $name = $rule->getId();
272 40
                $min  = $rule->from();
273
274 40
                $this->trace(new Entry($name, $min, null, $this->depth), $buffer);
0 ignored issues
show
Compatibility introduced by
$buffer of type object<Railt\Compiler\Iterator\BufferInterface> is not a sub-type of object<Railt\Compiler\Iterator\Buffer>. It seems like you assume a concrete implementation of the interface Railt\Compiler\Iterator\BufferInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
275 40
                \array_pop($this->todo);
276
277 40
                $this->todo[] = new Escape($name, $min, $this->todo);
278
279 40
                for ($i = 0; $i < $min; ++$i) {
280 30
                    $this->todo[] = new Escape($nextRule, 0);
281 30
                    $this->todo[] = new Entry($nextRule, 0);
282
                }
283
284 40
                return true;
285
            }
286
287 35
            $max = $rule->to();
288
289 35
            if ($max !== Repetition::INF_MAX_VALUE && $next > $max) {
290 1
                return false;
291
            }
292
293 35
            $this->todo[] = new Escape($rule->getId(), $next, $this->todo);
294 35
            $this->todo[] = new Escape($nextRule, 0);
295 35
            $this->todo[] = new Entry($nextRule, 0);
296
297 35
            return true;
298
        }
299
300
        return false;
301
    }
302
303
    /**
304
     * Get rule by name.
305
     *
306
     * @param $name
307
     * @return Symbol|null
308
     */
309 54
    public function getRule($name): ?Symbol
310
    {
311 54
        return $this->rules[$name] ?? null;
312
    }
313
314
    /**
315
     * Backtrack the trace.
316
     *
317
     * @param BufferInterface $buffer
318
     * @return bool
319
     */
320 46
    protected function backtrack(BufferInterface $buffer): bool
321
    {
322 46
        $found = false;
323
324
        do {
325 46
            $last = \array_pop($this->trace);
326
327 46
            if ($last instanceof Entry) {
328 34
                $found = $this->getRule($last->getRule()) instanceof Alternation;
329 46
            } elseif ($last instanceof Escape) {
330 41
                $found = $this->getRule($last->getRule()) instanceof Repetition;
331 33
            } elseif ($last instanceof Terminator) {
332 33
                $buffer->previous();
333
334 33
                if ($buffer->valid() === false) {
335
                    return false;
336
                }
337
            }
338 46
        } while (0 < \count($this->trace) && $found === false);
339
340 46
        if ($found === false) {
341
            return false;
342
        }
343
344 46
        $this->depth  = $last->getDepth();
345 46
        $this->todo   = $last->getTodo();
346 46
        $this->todo[] = new Entry($last->getRule(), $last->getData() + 1);
347
348 46
        return true;
349
    }
350
351
    /**
352
     * Build AST from trace.
353
     * Walk through the trace iteratively and recursively.
354
     *
355
     * @param int $i Current trace index.
356
     * @param array &$children Collected children.
357
     * @return Node|int
358
     */
359 54
    protected function buildTree($i = 0, array &$children = [])
360
    {
361 54
        $max = \count($this->trace);
362
363 54
        while ($i < $max) {
364
            /** @var Invocation|Terminator $trace */
365 54
            $trace = $this->trace[$i];
366
367 54
            if ($trace instanceof Entry) {
368 54
                $ruleName  = $trace->getRule();
369 54
                $rule      = $this->rules[$ruleName];
370 54
                $isRule    = $trace->isTransitional() === false;
371 54
                $nextTrace = $this->trace[$i + 1];
372 54
                $id        = $rule->getName();
373 54
                $offset    = $trace->getOffset();
374
375
                // Optimization: Skip empty trace sequence.
376 54
                if ($nextTrace instanceof Escape && $ruleName === $nextTrace->getRule()) {
377 5
                    $i += 2;
378
379 5
                    continue;
380
                }
381
382 54
                if ($isRule === true) {
383 54
                    $children[] = $ruleName;
384
                }
385
386 54
                if ($id !== null) {
387 54
                    $children[] = [
388 54
                        'id' => $id,
389
                    ];
390
                }
391
392 54
                $i = $this->buildTree($i + 1, $children);
393
394 54
                if ($isRule === false) {
395 52
                    continue;
396
                }
397
398 54
                $handle = [];
399 54
                $cId    = null;
400
401
                do {
402 54
                    $pop = \array_pop($children);
403
404 54
                    if (\is_object($pop) === true) {
405 52
                        $handle[] = $pop;
406 54
                    } elseif (\is_array($pop) === true && $cId === null) {
407 54
                        $cId = $pop['id'];
408 54
                    } elseif ($ruleName === $pop) {
409 54
                        break;
410
                    }
411 54
                } while ($pop !== null);
412
413 54
                if ($cId === null) {
414 20
                    for ($j = \count($handle) - 1; $j >= 0; --$j) {
415 20
                        $children[] = $handle[$j];
416
                    }
417
418 20
                    continue;
419
                }
420
421 54
                $rule       = new AstRule((string)($id ?: $cId), \array_reverse($handle), $offset);
422 54
                $children[] = $rule;
423 54
            } elseif ($trace instanceof Escape) {
424 54
                return $i + 1;
425
            } else {
426 54
                if ($trace->isKept() === false) {
0 ignored issues
show
Bug introduced by
The method isKept does only exist in Railt\Compiler\Parser\Trace\Terminator, but not in Railt\Compiler\Parser\Trace\Invocation.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
427 26
                    ++$i;
428
429 26
                    continue;
430
                }
431
432 52
                $children[] = new Leaf($trace->getName(), $trace->getValue(), $trace->getOffset());
0 ignored issues
show
Bug introduced by
The method getName does only exist in Railt\Compiler\Parser\Trace\Terminator, but not in Railt\Compiler\Parser\Trace\Invocation.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
Bug introduced by
The method getValue does only exist in Railt\Compiler\Parser\Trace\Terminator, but not in Railt\Compiler\Parser\Trace\Invocation.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
433 52
                ++$i;
434
            }
435
        }
436
437 54
        return $children[0];
438
    }
439
440
    /**
441
     * @return array
442
     */
443
    public function getRules(): array
444
    {
445
        return $this->rules;
446
    }
447
}
448