Template::expandMacro()   B
last analyzed

Complexity

Conditions 7
Paths 7

Size

Total Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 33
rs 8.4586
c 0
b 0
f 0
cc 7
nc 7
nop 3
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\CodeGen;
11
12
use PhpYacc\Compress\Compress;
13
use PhpYacc\Compress\CompressResult;
14
use PhpYacc\Exception\LogicException;
15
use PhpYacc\Exception\TemplateException;
16
use PhpYacc\Grammar\Context;
17
use PhpYacc\Support\Utils;
18
use PhpYacc\Yacc\Macro\DollarExpansion;
19
20
class Template
21
{
22
    /**
23
     * @var string
24
     */
25
    protected $metaChar = '$';
26
27
    /**
28
     * @var array
29
     */
30
    protected $template = [];
31
32
    /**
33
     * @var int
34
     */
35
    protected $lineNumber = 0;
36
37
    /**
38
     * @var bool
39
     */
40
    protected $copyHeader = false;
41
42
    /**
43
     * @var Context
44
     */
45
    protected $context;
46
47
    /**
48
     * @var CompressResult
49
     */
50
    protected $compress;
51
52
    /**
53
     * @var Language
54
     */
55
    protected $language;
56
57
    /**
58
     * Template constructor.
59
     *
60
     * @param Language $language
61
     * @param string   $template
62
     * @param Context  $context
63
     *
64
     * @throws TemplateException
65
     */
66
    public function __construct(Language $language, string $template, Context $context)
67
    {
68
        $this->language = $language;
69
        $this->context = $context;
70
71
        $this->parseTemplate($template);
72
    }
73
74
    /**
75
     * @param CompressResult $result
76
     * @param $resultFile
77
     * @param null $headerFile
78
     *
79
     * @throws LogicException
80
     * @throws TemplateException
81
     */
82
    public function render(CompressResult $result, $resultFile, $headerFile = null)
83
    {
84
        $headerFile = $headerFile ?: \fopen('php://memory', 'rw');
85
86
        $this->language->begin($resultFile, $headerFile);
87
88
        $this->compress = $result;
89
        unset($result);
90
91
        $skipMode = false;
92
        $lineChanged = false;
93
        $tailCode = false;
94
        $reduceMode = [
95
            'enabled' => false,
96
            'm'       => -1,
97
            'n'       => 0,
98
            'mac'     => [],
99
        ];
100
        $tokenMode = [
101
            'enabled' => false,
102
            'mac'     => [],
103
        ];
104
        $buffer = '';
105
106
        foreach ($this->template as $line) {
107
            $line .= "\n";
108
            if ($tailCode) {
109
                $this->language->write($buffer.$line);
110
                continue;
111
            }
112
113
            if ($skipMode) {
114
                if ($this->metaMatch(\ltrim($line), 'endif')) {
115
                    $skipMode = false;
116
                }
117
                continue;
118
            }
119
120
            if ($reduceMode['enabled']) {
121
                if ($this->metaMatch(\trim($line), 'endreduce')) {
122
                    $reduceMode['enabled'] = false;
123
                    $this->lineNumber++;
124
                    if ($reduceMode['m'] < 0) {
125
                        $reduceMode['m'] = $reduceMode['n'];
126
                    }
127
128
                    foreach ($this->context->grams as $gram) {
129
                        if ($gram->action) {
130
                            for ($j = 0; $j < $reduceMode['m']; $j++) {
131
                                $this->expandMacro($reduceMode['mac'][$j], $gram->num, null);
132
                            }
133
                        } else {
134
                            for ($j = $reduceMode['m']; $j < $reduceMode['n']; $j++) {
135
                                $this->expandMacro($reduceMode['mac'][$j], $gram->num, null);
136
                            }
137
                        }
138
                    }
139
                    continue;
140
                } elseif ($this->metaMatch(\trim($line), 'noact')) {
141
                    $reduceMode['m'] = $reduceMode['n'];
142
                    continue;
143
                }
144
                $reduceMode['mac'][$reduceMode['n']++] = $line;
145
                continue;
146
            }
147
148
            if ($tokenMode['enabled']) {
149
                if ($this->metaMatch(\trim($line), 'endtokenval')) {
150
                    $tokenMode['enabled'] = false;
151
                    $this->lineNumber++;
152
                    for ($i = 1; $i < $this->context->countTerminals; $i++) {
153
                        $symbol = $this->context->symbol($i);
154
                        if ($symbol->name[0] != '\'') {
155
                            $str = $symbol->name;
156
                            if ($i === 1) {
157
                                $str = 'YYERRTOK';
158
                            }
159
                            foreach ($tokenMode['mac'] as $mac) {
160
                                $this->expandMacro($mac, $symbol->value, $str);
161
                            }
162
                        }
163
                    }
164
                } else {
165
                    $tokenMode['mac'][] = $line;
166
                }
167
                continue;
168
            }
169
            $p = $line;
170
            $buffer = '';
171
            for ($i = 0; $i < \mb_strlen($line); $i++) {
172
                $p = \mb_substr($line, $i);
173
                if ($p[0] !== $this->metaChar) {
174
                    $buffer .= $line[$i];
175
                } elseif ($i + 1 < \mb_strlen($line) && $p[1] === $this->metaChar) {
176
                    $i++;
177
                    $buffer .= $this->metaChar;
178
                } elseif ($this->metaMatch($p, '(')) {
179
                    $start = $i + 2;
180
                    $val = \mb_substr($p, 2);
181
                    while ($i < \mb_strlen($line) && $line[$i] !== ')') {
182
                        $i++;
183
                    }
184
                    if (!isset($line[$i])) {
185
                        throw new TemplateException('$(: missing ")"');
186
                    }
187
                    $length = $i - $start;
188
189
                    $buffer .= $this->genValueOf(\mb_substr($val, 0, $length));
190
                } elseif ($this->metaMatch($p, 'TYPEOF(')) {
191
                    throw new LogicException('TYPEOF is not implemented');
192
                } else {
193
                    break;
194
                }
195
            }
196
            if (isset($p[0]) && $p[0] === $this->metaChar) {
197
                if (\trim($buffer) !== '') {
198
                    throw new TemplateException('Non-blank character before $-keyword');
199
                }
200
                if ($this->metaMatch($p, 'header')) {
201
                    $this->copyHeader = true;
202
                } elseif ($this->metaMatch($p, 'endheader')) {
203
                    $this->copyHeader = false;
204
                } elseif ($this->metaMatch($p, 'tailcode')) {
205
                    $this->printLine();
206
                    $tailCode = true;
207
                    continue;
208
                } elseif ($this->metaMatch($p, 'verification-table')) {
209
                    throw new TemplateException('verification-table is not implemented');
210
                } elseif ($this->metaMatch($p, 'union')) {
211
                    throw new TemplateException('union is not implemented');
212
                } elseif ($this->metaMatch($p, 'tokenval')) {
213
                    $tokenMode = [
214
                        'enabled' => true,
215
                        'mac'     => [],
216
                    ];
217
                } elseif ($this->metaMatch($p, 'reduce')) {
218
                    $reduceMode = [
219
                        'enabled' => true,
220
                        'm'       => -1,
221
                        'n'       => 0,
222
                        'mac'     => [],
223
                    ];
224
                } elseif ($this->metaMatch($p, 'switch-for-token-name')) {
225
                    for ($i = 0; $i < $this->context->countTerminals; $i++) {
226
                        if ($this->context->cTermIndex[$i] >= 0) {
227
                            $symbol = $this->context->symbol($i);
228
                            $this->language->caseBlock($buffer, $symbol->value, $symbol->name);
229
                        }
230
                    }
231
                } elseif ($this->metaMatch($p, 'production-strings')) {
232
                    foreach ($this->context->grams as $gram) {
233
                        $info = \array_slice($gram->body, 0);
234
235
                        $this->language->write($buffer.'"');
236
                        $this->language->writeQuoted($info[0]->name);
237
                        $this->language->writeQuoted(' :');
238
239
                        if (\count($info) === 1) {
240
                            $this->language->writeQuoted(' /* empty */');
241
                        }
242
243
                        for ($i = 1, $l = \count($info); $i < $l; $i++) {
244
                            $this->language->writeQuoted(' '.$info[$i]->name);
245
                        }
246
247
                        if ($gram->num + 1 === $this->context->countGrams) {
248
                            $this->language->write("\"\n");
249
                        } else {
250
                            $this->language->write("\",\n");
251
                        }
252
                    }
253
                } elseif ($this->metaMatch($p, 'listvar')) {
254
                    $var = \trim(\mb_substr($p, 9));
255
                    $this->genListVar($buffer, $var);
256
                } elseif ($this->metaMatch($p, 'ifnot')) {
257
                    $skipMode = $skipMode || !$this->skipIf($p);
258
                } elseif ($this->metaMatch($p, 'if')) {
259
                    $skipMode = $skipMode || $this->skipIf($p);
260
                } elseif ($this->metaMatch($p, 'endif')) {
261
                    $skipMode = false;
262
                } else {
263
                    throw new TemplateException("Unknown \$: $line");
264
                }
265
                $lineChanged = true;
266
            } else {
267
                if ($lineChanged) {
268
                    $this->printLine();
269
                    $lineChanged = false;
270
                }
271
                $this->language->write($buffer, $this->copyHeader);
272
            }
273
        }
274
275
        $this->language->commit();
276
    }
277
278
    /**
279
     * @param $spec string
280
     *
281
     * @throws TemplateException
282
     *
283
     * @return bool
284
     */
285
    protected function skipIf(string $spec): bool
286
    {
287
        [ $dump, $test ] = \explode(' ', $spec, 2);
0 ignored issues
show
Bug introduced by
The variable $dump does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $test seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
288
289
        $test = \trim($test);
0 ignored issues
show
Bug introduced by
The variable $test seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
290
        switch ($test) {
291
            case '-a':
292
                return $this->context->aflag;
293
294
            case '-t':
295
                return $this->context->tflag;
296
297
            case '-p':
298
                return (bool) $this->context->className;
299
300
            case '%union':
301
                return (bool) $this->context->unioned;
302
303
            case '%pure_parser':
304
                return $this->context->pureFlag;
305
306
            default:
307
                throw new TemplateException("$dump: unknown switch: $test");
308
        }
309
    }
310
311
    /**
312
     * @param string      $def
313
     * @param int         $value
314
     * @param string|null $str
315
     */
316
    protected function expandMacro(string $def, int $value, string $str = null)
317
    {
318
        $result = '';
319
        for ($i = 0; $i < \mb_strlen($def); $i++) {
320
            $p = $def[$i];
321
            if ($p === '%') {
322
                $p = $def[++$i];
323
                switch ($p) {
324
                    case 'n':
325
                        $result .= \sprintf('%d', $value);
326
                        break;
327
328
                    case 's':
329
                        $result .= $str !== null ? $str : '';
330
                        break;
331
332
                    case 'b':
333
                        $gram = $this->context->gram($value);
334
                        $this->printLine($gram->position);
335
                        $result .= $gram->action;
336
                        break;
337
338
                    default:
339
                        $result .= $p;
340
                        break;
341
                }
342
            } else {
343
                $result .= $p;
344
            }
345
        }
346
347
        $this->language->write($result, $this->copyHeader);
348
    }
349
350
    /**
351
     * @param string $indent
352
     * @param string $var
353
     *
354
     * @throws TemplateException
355
     */
356
    protected function genListVar(string $indent, string $var)
357
    {
358
        $size = -1;
359
        if (isset($this->compress->$var)) {
360
            $array = $this->compress->$var;
361
            if (isset($this->compress->{$var.'size'})) {
362
                $size = $this->compress->{$var.'size'};
363
            } elseif ($var === 'yydefault') {
364
                $size = $this->context->countNonLeafStates;
365
            } elseif (\in_array($var, ['yygbase', 'yygdefault'])) {
366
                $size = $this->context->countNonTerminals;
367
            } elseif (\in_array($var, ['yylhs', 'yylen'])) {
368
                $size = $this->context->countGrams;
369
            }
370
            $this->printArray($array, $size < 0 ? count($array) : $size, $indent);
371
        } elseif ($var === 'terminals') {
372
            $nl = 0;
373
            foreach ($this->context->terminals as $term) {
374
                if ($this->context->cTermIndex[$term->code] >= 0) {
375
                    $prefix = $nl++ ? ",\n" : '';
376
                    $this->language->write($prefix.$indent.'"');
377
                    $this->language->writeQuoted($term->name);
378
                    $this->language->write('"');
379
                }
380
            }
381
            $this->language->write("\n");
382
        } elseif ($var === 'nonterminals') {
383
            $nl = 0;
384
            foreach ($this->context->nonterminals as $nonterm) {
385
                $prefix = $nl++ ? ",\n" : '';
386
                $this->language->write($prefix.$indent.'"');
387
                $this->language->writeQuoted($nonterm->name);
388
                $this->language->write('"');
389
            }
390
            $this->language->write("\n");
391
        } else {
392
            throw new TemplateException("\$listvar: unknown variable $var");
393
        }
394
    }
395
396
    /**
397
     * @param array  $array
398
     * @param int    $limit
399
     * @param string $indent
400
     */
401
    protected function printArray(array $array, int $limit, string $indent)
402
    {
403
        $col = 0;
404
        for ($i = 0; $i < $limit; $i++) {
405
            if ($col === 0) {
406
                $this->language->write($indent);
407
            }
408
            $this->language->write(\sprintf($i + 1 === $limit ? '%5d' : '%5d,', $array[$i]));
409
            if (++$col === 10) {
410
                $this->language->write("\n");
411
                $col = 0;
412
            }
413
        }
414
        if ($col !== 0) {
415
            $this->language->write("\n");
416
        }
417
    }
418
419
    /**
420
     * @param string $var
421
     *
422
     * @throws TemplateException
423
     *
424
     * @return string
425
     */
426
    protected function genValueOf(string $var): string
427
    {
428
        switch ($var) {
429
            case 'YYSTATES':
430
                return \sprintf('%d', $this->context->countStates);
431
            case 'YYNLSTATES':
432
                return \sprintf('%d', $this->context->countNonLeafStates);
433
            case 'YYINTERRTOK':
434
                return \sprintf('%d', $this->compress->yytranslate[$this->context->errorToken->value]);
435
            case 'YYUNEXPECTED':
436
                return \sprintf('%d', Compress::UNEXPECTED);
437
            case 'YYDEFAULT':
438
                return \sprintf('%d', Compress::DEFAULT);
439
            case 'YYMAXLEX':
440
                return \sprintf('%d', \count($this->compress->yytranslate));
441
            case 'YYLAST':
442
                return \sprintf('%d', \count($this->compress->yyaction));
443
            case 'YYGLAST':
444
                return \sprintf('%d', \count($this->compress->yygoto));
445
            case 'YYTERMS':
446
            case 'YYBADCH':
447
                return \sprintf('%d', $this->compress->yyncterms);
448
            case 'YYNONTERMS':
449
                return \sprintf('%d', $this->context->countNonTerminals);
450
            case 'YY2TBLSTATE':
451
                return \sprintf('%d', $this->compress->yybasesize - $this->context->countNonLeafStates);
452
            case 'CLASSNAME':
453
            case '-p':
454
                return $this->context->className ?: 'yy';
455
            default:
456
                throw new TemplateException("Unknown variable: \$($var)");
457
        }
458
    }
459
460
    /**
461
     * @param string $template
462
     *
463
     * @throws TemplateException
464
     */
465
    protected function parseTemplate(string $template)
466
    {
467
        $template = \preg_replace("(\r\n|\r)", "\n", $template);
468
        $lines = \explode("\n", $template);
469
        $this->lineNumber = 1;
470
        $skip = false;
471
472
        foreach ($lines as $line) {
473
            $p = $line;
474
            if ($skip) {
475
                $this->template[] = $line;
476
                continue;
477
            }
478
            while (\mb_strlen($p) > 0 && Utils::isWhite($p[0])) {
479
                $p = \mb_substr($p, 1);
480
            }
481
            $this->lineNumber++;
482
            if ($this->metaMatch($p, 'include')) {
483
                $skip = true;
484
            } elseif ($this->metaMatch($p, 'meta')) {
485
                if (!isset($p[6]) || Utils::isWhite($p[6])) {
486
                    throw new TemplateException("\$meta: missing character in definition: $p");
487
                }
488
                $this->metaChar = $p[6];
489
            } elseif ($this->metaMatch($p, 'semval')) {
490
                $this->defSemvalMacro(\mb_substr($p, 7));
491
            } else {
492
                $this->template[] = $line;
493
            }
494
        }
495
    }
496
497
    /**
498
     * @param string $text
499
     * @param string $keyword
500
     *
501
     * @return bool
502
     */
503
    protected function metaMatch(string $text, string $keyword): bool
504
    {
505
        return isset($text[0]) && $text[0] === $this->metaChar && \mb_substr($text, 1, \mb_strlen($keyword)) === $keyword;
506
    }
507
508
    /**
509
     * @param string $macro
510
     *
511
     * @throws TemplateException
512
     */
513
    protected function defSemvalMacro(string $macro)
514
    {
515
        if (\mb_strpos($macro, '($)') !== false) {
516
            $this->context->macros[DollarExpansion::SEMVAL_LHS_UNTYPED] = \ltrim(\mb_substr($macro, 3));
517
        } elseif (\mb_strpos($macro, '($,%t)') !== false) {
518
            $this->context->macros[DollarExpansion::SEMVAL_LHS_TYPED] = \ltrim(\mb_substr($macro, 6));
519
        } elseif (\mb_strpos($macro, '(%n)') !== false) {
520
            $this->context->macros[DollarExpansion::SEMVAL_RHS_UNTYPED] = \ltrim(\mb_substr($macro, 4));
521
        } elseif (\mb_strpos($macro, '(%n,%t)') !== false) {
522
            $this->context->macros[DollarExpansion::SEMVAL_RHS_TYPED] = \ltrim(\mb_substr($macro, 7));
523
        } else {
524
            throw new TemplateException("\$semval: bad format $macro");
525
        }
526
    }
527
528
    /**
529
     * @param int         $line
530
     * @param string|null $filename
531
     */
532
    protected function printLine(int $line = -1, string $filename = null)
533
    {
534
        $line = $line === -1 ? $this->lineNumber : $line;
535
        $filename = $filename ?? $this->context->filename;
536
537
        $this->language->inlineComment(\sprintf('%s:%d', $filename, $line));
538
    }
539
}
540