Completed
Pull Request — master (#17)
by Roman
06:01 queued 02:35
created

Parser::parseDeclaration()   A

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
            $id = $head->getValue();
67
68
            if ($id == 'def') {
69
                return $this->parseDeclaration($tail);
70
            }
71
72
            if ($id == 'lambda') {
73
                return $this->parseLambdaDeclaration($tail);
74
            }
75
        }
76
77
        return $this->parseCallExpression($tokens);
78
    }
79
80
    /**
81
     * @param array $tokens
82
     * @return Nodes\Node
83
     */
84
    private function parseDeclaration($tokens): Nodes\Node
85
    {
86
        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...
87
88
        if (!is_array($head)) {
89
            return $this->parseAssignDeclaration($tokens);
90
        }
91
92
        return $this->parseFunctionDeclaration($tokens);
93
    }
94
95
    /**
96
     * @param $tokens
97
     * @return Nodes\Node
98
     * @throws ParserException
99
     */
100
    private function parseLambdaDeclaration($tokens): Nodes\Node
101
    {
102
        list ($head, $body) = toHeadTail($tokens);
103
104
        $test = all($head, function ($token) {
105
            return $token instanceof Tokens\IdentifierToken;
106
        });
107
108
        if (!$test) {
109
            throw new ParserException("Lambda arguments must be valid identifiers.");
110
        }
111
112
        $params = array_map([$this, 'parseToken'], $head);
113
114
        return new Nodes\LambdaExpression($params, $this->parseSequence($body));
115
    }
116
117
    /**
118
     * @param $tokens
119
     * @return Nodes\Node
120
     * @throws ParserException
121
     */
122
    private function parseFunctionDeclaration($tokens): Nodes\Node
123
    {
124
        list ($head, $body) = toHeadTail($tokens);
125
126
        if (sizeof($head) < 1) {
127
            throw new ParserException("Function must have identifier.");
128
        }
129
130
        $test = all($head, function ($token) {
131
            return $token instanceof Tokens\IdentifierToken;
132
        });
133
134
        if (!$test) {
135
            throw new ParserException("Function name and arguments must be valid identifiers.");
136
        }
137
138
        list ($name, $args) = toHeadTail($head);
139
140
        $params = array_map([$this, 'parseToken'], $args);
141
142
        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...
143
    }
144
145
    /**
146
     * @param array $tokens
147
     * @return Nodes\Assign
148
     * @throws ParserException
149
     */
150
    private function parseAssignDeclaration(array $tokens): Nodes\Assign
151
    {
152
        if (sizeof($tokens) % 2 != 0) {
153
            throw new ParserException("Bad assign declaration");
154
        }
155
156
        $assignments = array_chunk($tokens, 2);
157
158
        $result = array_map(function ($assign) {
159
            if (!$assign[0] instanceof Tokens\IdentifierToken) {
160
                throw new ParserException("Bad type of identifier in assign declaration");
161
            }
162
            return array_map([$this, 'parseToken'], $assign);
163
        }, $assignments);
164
165
        return new Nodes\Assign($result);
166
    }
167
168
    /**
169
     * @param array $tokens
170
     * @return Nodes\Node
171
     */
172
    private function parseCallExpression(array $tokens): Nodes\Node
173
    {
174
        list ($head, $tail) = toHeadTail($tokens);
175
176
        return new Nodes\CallExpression(
177
            $this->parseToken($head),
178
            array_map([$this, 'parseToken'], $tail)
179
        );
180
    }
181
182
    /**
183
     * @param array $tokens
184
     * @return Nodes\Program
185
     */
186
    private function parseProgram(array $tokens): Nodes\Program
187
    {
188
        return new Nodes\Program(array_map([$this, 'parseToken'], $tokens));
189
    }
190
191
    /**
192
     * @param array $tokens
193
     * @return Nodes\SequenceExpression
194
     */
195
    private function parseSequence(array $tokens): Nodes\SequenceExpression
196
    {
197
        return new Nodes\SequenceExpression(array_map([$this, 'parseToken'], $tokens));
198
    }
199
200
    /**
201
     * @param Tokens\IdentifierToken $token
202
     * @return Nodes\Identifier
203
     */
204
    private function parseIdentifier(Tokens\IdentifierToken $token): Nodes\Identifier
205
    {
206
        return new Nodes\Identifier($token->getValue());
207
    }
208
209
    /**
210
     * @param Tokens\StringToken $token
211
     * @return Nodes\Literal
212
     */
213
    private function parseString(Tokens\StringToken $token): Nodes\Literal
214
    {
215
        return new Nodes\StringNode($token->getValue());
216
    }
217
218
    /**
219
     * @param Tokens\NumericToken $token
220
     * @return Nodes\Literal
221
     */
222
    private function parseNumeric(Tokens\NumericToken $token): Nodes\Literal
223
    {
224
        return new Nodes\Number($token->getValue());
225
    }
226
227
    /**
228
     * Convert list of tokens into token tree.
229
     *
230
     * @param Tokens\Token[] $tokens
231
     * @return mixed
232
     */
233
    private function deflate(array $tokens)
234
    {
235
        $iter = tail(function ($rest, $acc) use (&$iter) {
236
            if (empty($rest)) {
237
                return $acc;
238
            }
239
            list ($head, $tail) = toHeadTail($rest);
240
            switch (get_class($head)) {
241
                case Tokens\OpenBracketToken::class:
242
                    $pairClosingIndex = $this->findPairClosingBracketIndex($tail);
243
                    $inner = array_slice($tail, 0, $pairClosingIndex);
244
                    $innerNode = $this->deflate($inner);
245
                    $newTail = array_slice($tail, $pairClosingIndex + 1);
246
                    return $iter($newTail, append($acc, $innerNode));
247
                case Tokens\CloseBracketToken::class:
248
                    throw new ParserException("Unpaired opening bracket found");
249
                default:
250
                    return $iter($tail, append($acc, $head));
251
            }
252
        });
253
        return $iter($tokens, []);
254
    }
255
256
    /**
257
     * @param array $tokens
258
     * @return mixed
259
     */
260
    private function findPairClosingBracketIndex(array $tokens)
261
    {
262
        $iter = tail(function ($rest, $depth, $position) use (&$iter) {
263
            if (empty($rest)) {
264
                throw new ParserException("Unpaired closing bracket found.");
265
            }
266
            list ($head, $tail) = toHeadTail($rest);
267
            if ($head instanceof Tokens\CloseBracketToken) {
268
                if ($depth == 0) {
269
                    return $position;
270
                }
271
                return $iter($tail, $depth - 1, $position + 1);
272
            }
273
            if ($head instanceof Tokens\OpenBracketToken) {
274
                return $iter($tail, $depth + 1, $position + 1);
275
            }
276
            return $iter($tail, $depth, $position + 1);
277
        });
278
        return $iter($tokens, 0, 0);
279
    }
280
}
281