Passed
Pull Request — master (#1)
by
unknown
02:40
created

FormulaParser::acceptTerm()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 18
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 10
c 1
b 0
f 0
nc 3
nop 1
dl 0
loc 18
ccs 0
cts 10
cp 0
crap 12
rs 9.9332
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Smoren\FormulaTools\V2;
6
7
use Smoren\FormulaTools\V2\Exceptions\{
8
    EmptyStreamException,
9
    MissingOperandException,
10
    MissingOperatorException,
11
    OpAssociationException,
12
    UnclosedBracketException,
13
};
14
use Smoren\FormulaTools\V2\Interfaces\{
15
    IBinaryOpAcceptor,
16
    IBinaryOpFactory,
17
    IDialect,
0 ignored issues
show
Bug introduced by
The type Smoren\FormulaTools\V2\Interfaces\IDialect was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
18
    IOpDefinition,
19
    IParser,
20
    IUnaryOpAcceptor,
21
    IUnaryOpFactory,
22
};
23
24
/**
25
 * @template Token
26
 * @template Result
27
 * @implements IParser<Token, Result>
28
 */
29
class FormulaParser implements IParser
30
{
31
    /** @var IDialect<Token, Result> */
32
    protected IDialect $dialect;
33
    protected int $position = 0;
34
    /** @var Result[] */
35
    protected array $resultStack = [];
36
    /** @var (IOpDefinition | IUnaryOpAcceptor<Token, Result>)[] */
0 ignored issues
show
Documentation Bug introduced by
The doc comment (IOpDefinition | IUnaryO...eptor<Token, Result>)[] at position 3 could not be parsed: Expected ')' at position 3, but found 'IUnaryOpAcceptor'.
Loading history...
37
    protected array $opStack = [];
38
    /** @var (IUnaryOpAcceptor<Token, Result> | IBinaryOpAcceptor<Token, Result>)[] */
0 ignored issues
show
Documentation Bug introduced by
The doc comment (IUnaryOpAcceptor<Token,...eptor<Token, Result>)[] at position 1 could not be parsed: Expected ')' at position 1, but found 'IUnaryOpAcceptor'.
Loading history...
39
    protected array $acceptorStack = [];
40
41
    /**
42
     * @param IDialect<Token, Result> $dialect
43
     */
44
    public function __construct(IDialect $dialect)
45
    {
46
        $this->dialect = $dialect;
47
    }
48
49
    public function getDialect(): IDialect
50
    {
51
        return $this->dialect;
52
    }
53
54
    public function setDialect(IDialect $dialect)
55
    {
56
        $this->dialect = $dialect;
57
    }
58
59
    public function getPosition(): int
60
    {
61
        return $this->position;
62
    }
63
64
    /**
65
     * @param IOpDefinition | IUnaryOpAcceptor<Token, Result> $op
66
     * @param int $followingPrecedence
67
     * @return bool
68
     * @phpstan-assert-if-true IUnaryOpFactory<Result> | IBinaryOpFactory<Result> $op
69
     */
70
    protected function shouldReduce($op, int $followingPrecedence): bool
71
    {
72
        if (!($op instanceof IUnaryOpFactory || $op instanceof IBinaryOpFactory)) {
73
            return false;
74
        }
75
        $prec = $op->getRightPrecedence();
76
        if ($prec !== $followingPrecedence) {
77
            return $prec > $followingPrecedence;
78
        }
79
        // An attempt to reduce a non-associative operator.
80
        throw new OpAssociationException($op, $this->position);
81
    }
82
83
    protected function reduce(int $followingPrecedence): void
84
    {
85
        $i = count($this->resultStack) - 1; // `i` always equals the index of the last node.
86
        for ($opIndex = count($this->opStack); $opIndex--; ) {
87
            $op = $this->opStack[$opIndex];
88
            if (!$this->shouldReduce($op, $followingPrecedence)) {
89
                return; // Reduction stops here.
90
            }
91
92
            // Perform one step of reduction.
93
            array_pop($this->opStack);
94
            if ($op instanceof IUnaryOpFactory) {
95
                // Reduce a unary operator.
96
                $this->resultStack[$i] = $op->create($this->resultStack[$i]);
97
            } else {
98
                // Reduce a binary operator.
99
                assert(count($this->resultStack) >= 2);
100
                $rhs = array_pop($this->resultStack);
101
                $i--;
102
                $this->resultStack[$i] = $op->create($this->resultStack[$i], $rhs);
103
            }
104
        }
105
    }
106
107
    /**
108
     * @param Token $token
0 ignored issues
show
Bug introduced by
The type Smoren\FormulaTools\V2\Token was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
109
     * @return bool
110
     */
111
    protected function acceptTerm($token): bool
112
    {
113
        $op = $this->dialect->acceptPrefixOrCircumfixOp($token, $this);
114
        if ($op === null) {
115
            // Not an operator - try to parse a term and push it onto the result stack.
116
            // This call may throw.
117
            $this->resultStack[] = $this->dialect->parseTerm($token, $this);
118
            return true;
119
        }
120
121
        if ($op instanceof IUnaryOpFactory) {
122
            $this->reduce($op->getLeftPrecedence()); // Check for non-associative operators.
123
        } else {
124
            $this->acceptorStack[] = $op; // Push the circumfix operator onto its stack.
125
        }
126
        // Push the unary/circumfix operator onto the operator stack.
127
        $this->opStack[] = $op;
128
        return false;
129
    }
130
131
    /**
132
     * @param Token $token
133
     * @return bool
134
     */
135
    protected function finishCircumfixOp($token): bool
136
    {
137
        if (!(bool)$this->acceptorStack) {
138
            return false;
139
        }
140
        // Check the token with the most deeply nested operator.
141
        $factory = $this->acceptorStack[count($this->acceptorStack) - 1]->accept($token);
142
        if ($factory === null) {
143
            return false;
144
        }
145
146
        // `token` is the second part of the *circumfix operator seen earlier.
147
        $this->reduce(PHP_INT_MIN); // Reduce the contained operand.
148
        $op = array_pop($this->opStack);
149
        array_pop($this->acceptorStack);
150
        if ($op instanceof IBinaryOpAcceptor) {
151
            assert(count($this->resultStack) >= 2);
152
            $rhs = array_pop($this->resultStack);
153
            $this->reduce($op->getLeftPrecedence()); // Reduce the left-hand-side operand.
154
            // We cannot reduce the postcircumfix operator right now because we need to check that
155
            // the operator that follows it respects associativity rules. So we push
156
            // the right-hand-side operand back onto the stack and create a stub binary operator
157
            // that remembers our right precedence.
158
            $this->resultStack[] = $rhs;
159
            $this->opStack[] = new class($op->getRightPrecedence(), $factory) implements IBinaryOpFactory {
160
                private int $precedence;
161
                /** @var callable(Result, Result): Result */
162
                private $factory;
163
164
                /**
165
                 * @param int $precedence
166
                 * @param callable(Result, Result): Result $factory
167
                 */
168
                public function __construct(int $precedence, callable $factory)
169
                {
170
                    $this->precedence = $precedence;
171
                    $this->factory = $factory;
172
                }
173
174
                public function getLeftPrecedence(): int {
175
                    throw new \LogicException("Unreachable");
176
                }
177
178
                public function getRightPrecedence(): int { return $this->precedence; }
179
                public function create($lhs, $rhs) { return ($this->factory)($lhs, $rhs); }
180
            };
181
        } else {
182
            // A circumfix operator does not have precedence. Reduce it right away.
183
            $i = count($this->resultStack) - 1;
184
            /** @var callable(Result): Result $factory */
185
            $this->resultStack[$i] = $factory($this->resultStack[$i]);
186
        }
187
        return true;
188
    }
189
190
    /**
191
     * @param Token $token
192
     * @return bool
193
     */
194
    protected function acceptBinaryOp($token): bool
195
    {
196
        // The second part of a *circumfix operator should be prioritized over a binary or postfix
197
        // operator, otherwise the former would be irreducible at all.
198
        if ($this->finishCircumfixOp($token)) {
199
            return false;
200
        }
201
202
        $op = $this->dialect->acceptPostfixOrInfixOp($token, $this);
203
        if ($op !== null) {
204
            $this->reduce($op->getLeftPrecedence()); // Reduce its left-hand-side operand.
205
            $this->opStack[] = $op;
206
            if ($op instanceof IBinaryOpAcceptor) {
207
                $this->acceptorStack[] = $op;
208
                return true; // It is binary.
209
            }
210
            return $op instanceof IBinaryOpFactory;
211
        }
212
213
        // We failed to recognize a binary operator in this dialect. Try juxtaposition if defined.
214
        $op = $this->dialect->getJuxtapositionOp($this);
215
        if ($op !== null) {
216
            $this->reduce($op->getLeftPrecedence()); // Reduce its left-hand-side operand.
217
            $this->opStack[] = $op;
218
            // If `token` is a term, then we should be waiting for another binary operator,
219
            // and vice versa.
220
            return !$this->acceptTerm($token);
221
        }
222
        throw new MissingOperatorException($token, $this->position);
223
    }
224
225
    /**
226
     * @param iterable<Token> $tokens
227
     * @return Result
0 ignored issues
show
Bug introduced by
The type Smoren\FormulaTools\V2\Result was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
228
     */
229
    public function parse(iterable $tokens)
230
    {
231
        assert(!(bool)$this->resultStack); // We are not reenterable.
232
        assert(!(bool)$this->opStack);
233
        $waitingForTerm = true;
234
        try {
235
            foreach ($tokens as $this->position => $token) {
236
                if ($waitingForTerm) {
237
                    if ($this->acceptTerm($token)) {
238
                        $waitingForTerm = false;
239
                    }
240
                } elseif ($this->acceptBinaryOp($token)) {
241
                    $waitingForTerm = true;
242
                }
243
            }
244
245
            if ($waitingForTerm) {
246
                throw count($this->resultStack) === 0 ? (
247
                    new EmptyStreamException()
248
                ) : new MissingOperandException();
249
            }
250
            $this->reduce(PHP_INT_MIN);
251
            if ((bool)$this->opStack) {
252
                throw new UnclosedBracketException();
253
            }
254
        } catch (\Throwable $e) {
255
            $this->resultStack = []; // Break a possible reference cycle.
256
            $this->opStack = [];
257
            $this->acceptorStack = [];
258
            throw $e;
259
        }
260
261
        assert(count($this->resultStack) === 1);
262
        return array_pop($this->resultStack);
263
    }
264
}
265