Parser::parseIfExpression()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 1
dl 0
loc 10
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace PeacefulBit\Slate\Parser;
4
5
use function Nerd\Common\Arrays\all;
6
use function Nerd\Common\Arrays\toHeadTail;
7
use function Nerd\Common\Arrays\append;
8
9
use function Nerd\Common\Functional\tail;
10
11
use PeacefulBit\Slate\Exceptions\ParserException;
12
use PeacefulBit\Slate\Parser\Nodes;
13
use PeacefulBit\Slate\Parser\Tokens;
14
15
class Parser
16
{
17
    /**
18
     * Convert tokens tree to abstract syntax tree.
19
     *
20
     * @param Tokens\Token[] $tokens
21
     * @return Nodes\Node
22
     */
23
    public function parse(array $tokens): Nodes\Node
24
    {
25
        $tokensTree = $this->deflate($tokens);
26
27
        return $this->parseProgram($tokensTree);
28
    }
29
30
    /**
31
     * @param $token
32
     * @return Nodes\Identifier|Nodes\Literal|Nodes\Node
33
     * @throws ParserException
34
     */
35
    private function parseToken($token)
36
    {
37
        if (is_array($token)) {
38
            return $this->parseExpression($token);
39
        }
40
        if ($token instanceof Tokens\StringToken) {
41
            return $this->parseString($token);
42
        }
43
        if ($token instanceof Tokens\IdentifierToken) {
44
            return $this->parseIdentifier($token);
45
        }
46
        if ($token instanceof Tokens\NumericToken) {
47
            return $this->parseNumeric($token);
48
        }
49
        throw new ParserException("Unexpected type of token - $token.");
50
    }
51
52
    /**
53
     * @param array $tokens
54
     * @return Nodes\Node
55
     * @throws ParserException
56
     */
57
    private function parseExpression(array $tokens): Nodes\Node
58
    {
59
        if (empty($tokens)) {
60
            throw new ParserException("Expression could not be empty.");
61
        }
62
63
        list ($head, $tail) = toHeadTail($tokens);
64
65
        if ($head instanceof Tokens\IdentifierToken) {
66
            switch ($head->getValue()) {
67
                case 'def':
68
                    return $this->parseDeclaration($tail);
69
                case 'lambda':
70
                    return $this->parseLambdaDeclaration($tail);
71
                case 'if':
72
                    return $this->parseIfExpression($tail);
73
                case 'or':
74
                    return $this->parseOrExpression($tail);
75
                case 'and':
76
                    return $this->parseAndExpression($tail);
77
                case 'use':
78
                case 'cond':
79
                    throw new ParserException('This keyword is reserved');
80
            }
81
        }
82
83
        return $this->parseCallExpression($tokens);
84
    }
85
86
    /**
87
     * @param array $tokens
88
     * @return Nodes\Node
89
     */
90
    private function parseDeclaration($tokens): Nodes\Node
91
    {
92
        list ($head, $tail) = $tokens;
0 ignored issues
show
Unused Code introduced by
The assignment to $tail is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
93
94
        if (!is_array($head)) {
95
            return $this->parseAssignDeclaration($tokens);
96
        }
97
98
        return $this->parseFunctionDeclaration($tokens);
99
    }
100
101
    /**
102
     * @param $tokens
103
     * @return Nodes\Node
104
     * @throws ParserException
105
     */
106
    private function parseIfExpression($tokens): Nodes\Node
107
    {
108
        if (sizeof($tokens) != 3) {
109
            throw new ParserException("'if' requires exactly three arguments");
110
        }
111
112
        list ($test, $cons, $alt) = array_map([$this, 'parseToken'], $tokens);
113
114
        return new Nodes\IfExpression($test, $cons, $alt);
115
    }
116
117
    /**
118
     * @param $tokens
119
     * @return Nodes\Node
120
     * @throws ParserException
121
     */
122 View Code Duplication
    private function parseOrExpression($tokens): Nodes\Node
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
123
    {
124
        if (empty($tokens)) {
125
            throw new ParserException("'or' requires at least one argument");
126
        }
127
128
        $expressions = array_map([$this, 'parseToken'], $tokens);
129
130
        return new Nodes\OrExpression($expressions);
131
    }
132
133
    /**
134
     * @param $tokens
135
     * @return Nodes\Node
136
     * @throws ParserException
137
     */
138 View Code Duplication
    private function parseAndExpression($tokens): Nodes\Node
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
139
    {
140
        if (empty($tokens)) {
141
            throw new ParserException("'and' requires at least one argument");
142
        }
143
144
        $expressions = array_map([$this, 'parseToken'], $tokens);
145
146
        return new Nodes\AndExpression($expressions);
147
    }
148
149
    /**
150
     * @param $tokens
151
     * @return Nodes\Node
152
     * @throws ParserException
153
     */
154
    private function parseLambdaDeclaration($tokens): Nodes\Node
155
    {
156
        list ($head, $body) = toHeadTail($tokens);
157
158
        $test = all($head, function ($token) {
159
            return $token instanceof Tokens\IdentifierToken;
160
        });
161
162
        if (!$test) {
163
            throw new ParserException("Lambda arguments must be valid identifiers.");
164
        }
165
166
        $params = array_map([$this, 'parseToken'], $head);
167
168
        return new Nodes\LambdaExpression($params, $this->parseSequence($body));
169
    }
170
171
    /**
172
     * @param $tokens
173
     * @return Nodes\Node
174
     * @throws ParserException
175
     */
176
    private function parseFunctionDeclaration($tokens): Nodes\Node
177
    {
178
        list ($head, $body) = toHeadTail($tokens);
179
180
        if (sizeof($head) < 1) {
181
            throw new ParserException("Function must have identifier.");
182
        }
183
184
        $test = all($head, function ($token) {
185
            return $token instanceof Tokens\IdentifierToken;
186
        });
187
188
        if (!$test) {
189
            throw new ParserException("Function name and arguments must be valid identifiers.");
190
        }
191
192
        list ($name, $args) = toHeadTail($head);
193
194
        $params = array_map([$this, 'parseToken'], $args);
195
196
        return new Nodes\FunctionExpression($name->getValue(), $params, $this->parseSequence($body));
0 ignored issues
show
Bug introduced by
The method getValue cannot be called on $name (of type null).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
197
    }
198
199
    /**
200
     * @param array $tokens
201
     * @return Nodes\Assign
202
     * @throws ParserException
203
     */
204
    private function parseAssignDeclaration(array $tokens): Nodes\Assign
205
    {
206
        if (sizeof($tokens) % 2 != 0) {
207
            throw new ParserException("Bad assign declaration");
208
        }
209
210
        $assignments = array_chunk($tokens, 2);
211
212
        $result = array_map(function ($assign) {
213
            if (!$assign[0] instanceof Tokens\IdentifierToken) {
214
                throw new ParserException("Bad type of identifier in assign declaration");
215
            }
216
            return array_map([$this, 'parseToken'], $assign);
217
        }, $assignments);
218
219
        return new Nodes\Assign($result);
220
    }
221
222
    /**
223
     * @param array $tokens
224
     * @return Nodes\Node
225
     */
226
    private function parseCallExpression(array $tokens): Nodes\Node
227
    {
228
        list ($head, $tail) = toHeadTail($tokens);
229
230
        return new Nodes\CallExpression(
231
            $this->parseToken($head),
232
            array_map([$this, 'parseToken'], $tail)
233
        );
234
    }
235
236
    /**
237
     * @param array $tokens
238
     * @return Nodes\Program
239
     */
240
    private function parseProgram(array $tokens): Nodes\Program
241
    {
242
        return new Nodes\Program(array_map([$this, 'parseToken'], $tokens));
243
    }
244
245
    /**
246
     * @param array $tokens
247
     * @return Nodes\SequenceExpression
248
     */
249
    private function parseSequence(array $tokens): Nodes\SequenceExpression
250
    {
251
        return new Nodes\SequenceExpression(array_map([$this, 'parseToken'], $tokens));
252
    }
253
254
    /**
255
     * @param Tokens\IdentifierToken $token
256
     * @return Nodes\Identifier
257
     */
258
    private function parseIdentifier(Tokens\IdentifierToken $token): Nodes\Identifier
259
    {
260
        return new Nodes\Identifier($token->getValue());
261
    }
262
263
    /**
264
     * @param Tokens\StringToken $token
265
     * @return Nodes\Literal
266
     */
267
    private function parseString(Tokens\StringToken $token): Nodes\Literal
268
    {
269
        return new Nodes\StringNode($token->getValue());
270
    }
271
272
    /**
273
     * @param Tokens\NumericToken $token
274
     * @return Nodes\Literal
275
     */
276
    private function parseNumeric(Tokens\NumericToken $token): Nodes\Literal
277
    {
278
        return new Nodes\Number($token->getValue());
279
    }
280
281
    /**
282
     * Convert list of tokens into token tree.
283
     *
284
     * @param Tokens\Token[] $tokens
285
     * @return mixed
286
     */
287
    private function deflate(array $tokens)
288
    {
289
        $iter = tail(function ($rest, $acc) use (&$iter) {
290
            if (empty($rest)) {
291
                return $acc;
292
            }
293
            list ($head, $tail) = toHeadTail($rest);
294
            switch (get_class($head)) {
295
                case Tokens\OpenBracketToken::class:
296
                    $pairClosingIndex = $this->findPairClosingBracketIndex($tail);
297
                    $inner = array_slice($tail, 0, $pairClosingIndex);
298
                    $innerNode = $this->deflate($inner);
299
                    $newTail = array_slice($tail, $pairClosingIndex + 1);
300
                    return $iter($newTail, append($acc, $innerNode));
301
                case Tokens\CloseBracketToken::class:
302
                    throw new ParserException("Unpaired opening bracket found");
303
                default:
304
                    return $iter($tail, append($acc, $head));
305
            }
306
        });
307
        return $iter($tokens, []);
308
    }
309
310
    /**
311
     * @param array $tokens
312
     * @return mixed
313
     */
314
    private function findPairClosingBracketIndex(array $tokens)
315
    {
316
        $iter = tail(function ($rest, $depth, $position) use (&$iter) {
317
            if (empty($rest)) {
318
                throw new ParserException("Unpaired closing bracket found.");
319
            }
320
            list ($head, $tail) = toHeadTail($rest);
321
            if ($head instanceof Tokens\CloseBracketToken) {
322
                if ($depth == 0) {
323
                    return $position;
324
                }
325
                return $iter($tail, $depth - 1, $position + 1);
326
            }
327
            if ($head instanceof Tokens\OpenBracketToken) {
328
                return $iter($tail, $depth + 1, $position + 1);
329
            }
330
            return $iter($tail, $depth, $position + 1);
331
        });
332
        return $iter($tokens, 0, 0);
333
    }
334
}
335