1<?php
2/**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
16 *
17 */
18
19namespace MediaWiki\Extensions\ParserFunctions;
20
21use UtfNormal\Validator;
22
23// @codeCoverageIgnoreStart
24
25// Character classes
26define( 'EXPR_WHITE_CLASS', " \t\r\n" );
27define( 'EXPR_NUMBER_CLASS', '0123456789.' );
28
29// Token types
30define( 'EXPR_WHITE', 1 );
31define( 'EXPR_NUMBER', 2 );
32define( 'EXPR_NEGATIVE', 3 );
33define( 'EXPR_POSITIVE', 4 );
34define( 'EXPR_PLUS', 5 );
35define( 'EXPR_MINUS', 6 );
36define( 'EXPR_TIMES', 7 );
37define( 'EXPR_DIVIDE', 8 );
38define( 'EXPR_MOD', 9 );
39define( 'EXPR_OPEN', 10 );
40define( 'EXPR_CLOSE', 11 );
41define( 'EXPR_AND', 12 );
42define( 'EXPR_OR', 13 );
43define( 'EXPR_NOT', 14 );
44define( 'EXPR_EQUALITY', 15 );
45define( 'EXPR_LESS', 16 );
46define( 'EXPR_GREATER', 17 );
47define( 'EXPR_LESSEQ', 18 );
48define( 'EXPR_GREATEREQ', 19 );
49define( 'EXPR_NOTEQ', 20 );
50define( 'EXPR_ROUND', 21 );
51define( 'EXPR_EXPONENT', 22 );
52define( 'EXPR_SINE', 23 );
53define( 'EXPR_COSINE', 24 );
54define( 'EXPR_TANGENS', 25 );
55define( 'EXPR_ARCSINE', 26 );
56define( 'EXPR_ARCCOS', 27 );
57define( 'EXPR_ARCTAN', 28 );
58define( 'EXPR_EXP', 29 );
59define( 'EXPR_LN', 30 );
60define( 'EXPR_ABS', 31 );
61define( 'EXPR_FLOOR', 32 );
62define( 'EXPR_TRUNC', 33 );
63define( 'EXPR_CEIL', 34 );
64define( 'EXPR_POW', 35 );
65define( 'EXPR_PI', 36 );
66define( 'EXPR_FMOD', 37 );
67define( 'EXPR_SQRT', 38 );
68
69// @codeCoverageIgnoreEnd
70
71class ExprParser {
72	public $maxStackSize = 100;
73
74	public $precedence = [
75		EXPR_NEGATIVE => 10,
76		EXPR_POSITIVE => 10,
77		EXPR_EXPONENT => 10,
78		EXPR_SINE => 9,
79		EXPR_COSINE => 9,
80		EXPR_TANGENS => 9,
81		EXPR_ARCSINE => 9,
82		EXPR_ARCCOS => 9,
83		EXPR_ARCTAN => 9,
84		EXPR_EXP => 9,
85		EXPR_LN => 9,
86		EXPR_ABS => 9,
87		EXPR_FLOOR => 9,
88		EXPR_TRUNC => 9,
89		EXPR_CEIL => 9,
90		EXPR_NOT => 9,
91		EXPR_SQRT => 9,
92		EXPR_POW => 8,
93		EXPR_TIMES => 7,
94		EXPR_DIVIDE => 7,
95		EXPR_MOD => 7,
96		EXPR_FMOD => 7,
97		EXPR_PLUS => 6,
98		EXPR_MINUS => 6,
99		EXPR_ROUND => 5,
100		EXPR_EQUALITY => 4,
101		EXPR_LESS => 4,
102		EXPR_GREATER => 4,
103		EXPR_LESSEQ => 4,
104		EXPR_GREATEREQ => 4,
105		EXPR_NOTEQ => 4,
106		EXPR_AND => 3,
107		EXPR_OR => 2,
108		EXPR_PI => 0,
109		EXPR_OPEN => -1,
110		EXPR_CLOSE => -1,
111	];
112
113	public $names = [
114		EXPR_NEGATIVE => '-',
115		EXPR_POSITIVE => '+',
116		EXPR_NOT => 'not',
117		EXPR_TIMES => '*',
118		EXPR_DIVIDE => '/',
119		EXPR_MOD => 'mod',
120		EXPR_FMOD => 'fmod',
121		EXPR_PLUS => '+',
122		EXPR_MINUS => '-',
123		EXPR_ROUND => 'round',
124		EXPR_EQUALITY => '=',
125		EXPR_LESS => '<',
126		EXPR_GREATER => '>',
127		EXPR_LESSEQ => '<=',
128		EXPR_GREATEREQ => '>=',
129		EXPR_NOTEQ => '<>',
130		EXPR_AND => 'and',
131		EXPR_OR => 'or',
132		EXPR_EXPONENT => 'e',
133		EXPR_SINE => 'sin',
134		EXPR_COSINE => 'cos',
135		EXPR_TANGENS => 'tan',
136		EXPR_ARCSINE => 'asin',
137		EXPR_ARCCOS => 'acos',
138		EXPR_ARCTAN => 'atan',
139		EXPR_LN => 'ln',
140		EXPR_EXP => 'exp',
141		EXPR_ABS => 'abs',
142		EXPR_FLOOR => 'floor',
143		EXPR_TRUNC => 'trunc',
144		EXPR_CEIL => 'ceil',
145		EXPR_POW => '^',
146		EXPR_PI => 'pi',
147		EXPR_SQRT => 'sqrt',
148	];
149
150	public $words = [
151		'mod' => EXPR_MOD,
152		'fmod' => EXPR_FMOD,
153		'and' => EXPR_AND,
154		'or' => EXPR_OR,
155		'not' => EXPR_NOT,
156		'round' => EXPR_ROUND,
157		'div' => EXPR_DIVIDE,
158		'e' => EXPR_EXPONENT,
159		'sin' => EXPR_SINE,
160		'cos' => EXPR_COSINE,
161		'tan' => EXPR_TANGENS,
162		'asin' => EXPR_ARCSINE,
163		'acos' => EXPR_ARCCOS,
164		'atan' => EXPR_ARCTAN,
165		'exp' => EXPR_EXP,
166		'ln' => EXPR_LN,
167		'abs' => EXPR_ABS,
168		'trunc' => EXPR_TRUNC,
169		'floor' => EXPR_FLOOR,
170		'ceil' => EXPR_CEIL,
171		'pi' => EXPR_PI,
172		'sqrt' => EXPR_SQRT,
173	];
174
175	/**
176	 * Evaluate a mathematical expression
177	 *
178	 * The algorithm here is based on the infix to RPN algorithm given in
179	 * http://montcs.bloomu.edu/~bobmon/Information/RPN/infix2rpn.shtml
180	 * It's essentially the same as Dijkstra's shunting yard algorithm.
181	 * @param string $expr
182	 * @return string
183	 * @throws ExprError
184	 */
185	public function doExpression( $expr ) {
186		$operands = [];
187		$operators = [];
188
189		# Unescape inequality operators
190		$expr = strtr( $expr, [ '&lt;' => '<', '&gt;' => '>',
191			'&minus;' => '-', '−' => '-' ] );
192
193		$p = 0;
194		$end = strlen( $expr );
195		$expecting = 'expression';
196		$name = '';
197
198		while ( $p < $end ) {
199			if ( count( $operands ) > $this->maxStackSize || count( $operators ) > $this->maxStackSize ) {
200				throw new ExprError( 'stack_exhausted' );
201			}
202			$char = $expr[$p];
203			$char2 = substr( $expr, $p, 2 );
204
205			// Mega if-elseif-else construct
206			// Only binary operators fall through for processing at the bottom, the rest
207			// finish their processing and continue
208
209			// First the unlimited length classes
210
211			// @phan-suppress-next-line PhanParamSuspiciousOrder false positive
212			if ( false !== strpos( EXPR_WHITE_CLASS, $char ) ) {
213				// Whitespace
214				$p += strspn( $expr, EXPR_WHITE_CLASS, $p );
215				continue;
216				// @phan-suppress-next-line PhanParamSuspiciousOrder false positive
217			} elseif ( false !== strpos( EXPR_NUMBER_CLASS, $char ) ) {
218				// Number
219				if ( $expecting !== 'expression' ) {
220					throw new ExprError( 'unexpected_number' );
221				}
222
223				// Find the rest of it
224				$length = strspn( $expr, EXPR_NUMBER_CLASS, $p );
225				// Convert it to float, silently removing double decimal points
226				$operands[] = (float)substr( $expr, $p, $length );
227				$p += $length;
228				$expecting = 'operator';
229				continue;
230			} elseif ( ctype_alpha( $char ) ) {
231				// Word
232				// Find the rest of it
233				$remaining = substr( $expr, $p );
234				if ( !preg_match( '/^[A-Za-z]*/', $remaining, $matches ) ) {
235					// This should be unreachable
236					throw new ExprError( 'preg_match_failure' );
237				}
238				$word = strtolower( $matches[0] );
239				$p += strlen( $word );
240
241				// Interpret the word
242				if ( !isset( $this->words[$word] ) ) {
243					throw new ExprError( 'unrecognised_word', $word );
244				}
245				$op = $this->words[$word];
246				switch ( $op ) {
247					// constant
248					case EXPR_EXPONENT:
249						if ( $expecting !== 'expression' ) {
250							break;
251						}
252						$operands[] = exp( 1 );
253						$expecting = 'operator';
254						continue 2;
255					case EXPR_PI:
256						if ( $expecting !== 'expression' ) {
257							throw new ExprError( 'unexpected_number' );
258						}
259						$operands[] = pi();
260						$expecting = 'operator';
261						continue 2;
262					// Unary operator
263					case EXPR_NOT:
264					case EXPR_SINE:
265					case EXPR_COSINE:
266					case EXPR_TANGENS:
267					case EXPR_ARCSINE:
268					case EXPR_ARCCOS:
269					case EXPR_ARCTAN:
270					case EXPR_EXP:
271					case EXPR_LN:
272					case EXPR_ABS:
273					case EXPR_FLOOR:
274					case EXPR_TRUNC:
275					case EXPR_CEIL:
276					case EXPR_SQRT:
277						if ( $expecting !== 'expression' ) {
278							throw new ExprError( 'unexpected_operator', $word );
279						}
280						$operators[] = $op;
281						continue 2;
282				}
283				// Binary operator, fall through
284				$name = $word;
285			} elseif ( $char2 === '<=' ) {
286				$name = $char2;
287				$op = EXPR_LESSEQ;
288				$p += 2;
289			} elseif ( $char2 === '>=' ) {
290				$name = $char2;
291				$op = EXPR_GREATEREQ;
292				$p += 2;
293			} elseif ( $char2 === '<>' || $char2 === '!=' ) {
294				$name = $char2;
295				$op = EXPR_NOTEQ;
296				$p += 2;
297			} elseif ( $char === '+' ) {
298				++$p;
299				if ( $expecting === 'expression' ) {
300					// Unary plus
301					$operators[] = EXPR_POSITIVE;
302					continue;
303				} else {
304					// Binary plus
305					$op = EXPR_PLUS;
306				}
307			} elseif ( $char === '-' ) {
308				++$p;
309				if ( $expecting === 'expression' ) {
310					// Unary minus
311					$operators[] = EXPR_NEGATIVE;
312					continue;
313				} else {
314					// Binary minus
315					$op = EXPR_MINUS;
316				}
317			} elseif ( $char === '*' ) {
318				$name = $char;
319				$op = EXPR_TIMES;
320				++$p;
321			} elseif ( $char === '/' ) {
322				$name = $char;
323				$op = EXPR_DIVIDE;
324				++$p;
325			} elseif ( $char === '^' ) {
326				$name = $char;
327				$op = EXPR_POW;
328				++$p;
329			} elseif ( $char === '(' ) {
330				if ( $expecting === 'operator' ) {
331					throw new ExprError( 'unexpected_operator', '(' );
332				}
333				$operators[] = EXPR_OPEN;
334				++$p;
335				continue;
336			} elseif ( $char === ')' ) {
337				$lastOp = end( $operators );
338				while ( $lastOp && $lastOp != EXPR_OPEN ) {
339					$this->doOperation( $lastOp, $operands );
340					array_pop( $operators );
341					$lastOp = end( $operators );
342				}
343				if ( $lastOp ) {
344					array_pop( $operators );
345				} else {
346					throw new ExprError( 'unexpected_closing_bracket' );
347				}
348				$expecting = 'operator';
349				++$p;
350				continue;
351			} elseif ( $char === '=' ) {
352				$name = $char;
353				$op = EXPR_EQUALITY;
354				++$p;
355			} elseif ( $char === '<' ) {
356				$name = $char;
357				$op = EXPR_LESS;
358				++$p;
359			} elseif ( $char === '>' ) {
360				$name = $char;
361				$op = EXPR_GREATER;
362				++$p;
363			} else {
364				$utfExpr = Validator::cleanUp( substr( $expr, $p ) );
365				throw new ExprError( 'unrecognised_punctuation', mb_substr( $utfExpr, 0, 1 ) );
366			}
367
368			// Binary operator processing
369			if ( $expecting === 'expression' ) {
370				throw new ExprError( 'unexpected_operator', $name );
371			}
372
373			// Shunting yard magic
374			$lastOp = end( $operators );
375			while ( $lastOp && $this->precedence[$op] <= $this->precedence[$lastOp] ) {
376				$this->doOperation( $lastOp, $operands );
377				array_pop( $operators );
378				$lastOp = end( $operators );
379			}
380			$operators[] = $op;
381			$expecting = 'expression';
382		}
383
384		// Finish off the operator array
385		// @codingStandardsIgnoreStart
386		while ( $op = array_pop( $operators ) ) {
387		// @codingStandardsIgnoreEnd
388			if ( $op == EXPR_OPEN ) {
389				throw new ExprError( 'unclosed_bracket' );
390			}
391			$this->doOperation( $op, $operands );
392		}
393
394		return implode( "<br />\n", $operands );
395	}
396
397	/**
398	 * @param int $op
399	 * @param array &$stack
400	 * @throws ExprError
401	 */
402	public function doOperation( $op, &$stack ) {
403		switch ( $op ) {
404			case EXPR_NEGATIVE:
405				if ( count( $stack ) < 1 ) {
406					throw new ExprError( 'missing_operand', $this->names[$op] );
407				}
408				$arg = array_pop( $stack );
409				$stack[] = -$arg;
410				break;
411			case EXPR_POSITIVE:
412				if ( count( $stack ) < 1 ) {
413					throw new ExprError( 'missing_operand', $this->names[$op] );
414				}
415				break;
416			case EXPR_TIMES:
417				if ( count( $stack ) < 2 ) {
418					throw new ExprError( 'missing_operand', $this->names[$op] );
419				}
420				$right = array_pop( $stack );
421				$left = array_pop( $stack );
422				$stack[] = $left * $right;
423					break;
424			case EXPR_DIVIDE:
425				if ( count( $stack ) < 2 ) {
426					throw new ExprError( 'missing_operand', $this->names[$op] );
427				}
428				$right = array_pop( $stack );
429				$left = array_pop( $stack );
430				if ( !$right ) {
431					throw new ExprError( 'division_by_zero', $this->names[$op] );
432				}
433				$stack[] = $left / $right;
434				break;
435			case EXPR_MOD:
436				if ( count( $stack ) < 2 ) {
437					throw new ExprError( 'missing_operand', $this->names[$op] );
438				}
439				$right = (int)array_pop( $stack );
440				$left = (int)array_pop( $stack );
441				if ( !$right ) {
442					throw new ExprError( 'division_by_zero', $this->names[$op] );
443				}
444				$stack[] = $left % $right;
445				break;
446			case EXPR_FMOD:
447				if ( count( $stack ) < 2 ) {
448					throw new ExprError( 'missing_operand', $this->names[$op] );
449				}
450				$right = (double)array_pop( $stack );
451				$left = (double)array_pop( $stack );
452				if ( !$right ) {
453					throw new ExprError( 'division_by_zero', $this->names[$op] );
454				}
455				$stack[] = fmod( $left, $right );
456				break;
457			case EXPR_PLUS:
458				if ( count( $stack ) < 2 ) {
459					throw new ExprError( 'missing_operand', $this->names[$op] );
460				}
461				$right = array_pop( $stack );
462				$left = array_pop( $stack );
463				$stack[] = $left + $right;
464				break;
465			case EXPR_MINUS:
466				if ( count( $stack ) < 2 ) {
467					throw new ExprError( 'missing_operand', $this->names[$op] );
468				}
469				$right = array_pop( $stack );
470				$left = array_pop( $stack );
471				$stack[] = $left - $right;
472				break;
473			case EXPR_AND:
474				if ( count( $stack ) < 2 ) {
475					throw new ExprError( 'missing_operand', $this->names[$op] );
476				}
477				$right = array_pop( $stack );
478				$left = array_pop( $stack );
479				$stack[] = ( $left && $right ) ? 1 : 0;
480				break;
481			case EXPR_OR:
482				if ( count( $stack ) < 2 ) {
483					throw new ExprError( 'missing_operand', $this->names[$op] );
484				}
485				$right = array_pop( $stack );
486				$left = array_pop( $stack );
487				$stack[] = ( $left || $right ) ? 1 : 0;
488				break;
489			case EXPR_EQUALITY:
490				if ( count( $stack ) < 2 ) {
491					throw new ExprError( 'missing_operand', $this->names[$op] );
492				}
493				$right = array_pop( $stack );
494				$left = array_pop( $stack );
495				$stack[] = ( $left == $right ) ? 1 : 0;
496				break;
497			case EXPR_NOT:
498				if ( count( $stack ) < 1 ) {
499					throw new ExprError( 'missing_operand', $this->names[$op] );
500				}
501				$arg = array_pop( $stack );
502				$stack[] = ( !$arg ) ? 1 : 0;
503				break;
504			case EXPR_ROUND:
505				if ( count( $stack ) < 2 ) {
506					throw new ExprError( 'missing_operand', $this->names[$op] );
507				}
508				$digits = (int)array_pop( $stack );
509				$value = array_pop( $stack );
510				$stack[] = round( $value, $digits );
511				break;
512			case EXPR_LESS:
513				if ( count( $stack ) < 2 ) {
514					throw new ExprError( 'missing_operand', $this->names[$op] );
515				}
516				$right = array_pop( $stack );
517				$left = array_pop( $stack );
518				$stack[] = ( $left < $right ) ? 1 : 0;
519				break;
520			case EXPR_GREATER:
521				if ( count( $stack ) < 2 ) {
522					throw new ExprError( 'missing_operand', $this->names[$op] );
523				}
524				$right = array_pop( $stack );
525				$left = array_pop( $stack );
526				$stack[] = ( $left > $right ) ? 1 : 0;
527				break;
528			case EXPR_LESSEQ:
529				if ( count( $stack ) < 2 ) {
530					throw new ExprError( 'missing_operand', $this->names[$op] );
531				}
532				$right = array_pop( $stack );
533				$left = array_pop( $stack );
534				$stack[] = ( $left <= $right ) ? 1 : 0;
535				break;
536			case EXPR_GREATEREQ:
537				if ( count( $stack ) < 2 ) {
538					throw new ExprError( 'missing_operand', $this->names[$op] );
539				}
540				$right = array_pop( $stack );
541				$left = array_pop( $stack );
542				$stack[] = ( $left >= $right ) ? 1 : 0;
543				break;
544			case EXPR_NOTEQ:
545				if ( count( $stack ) < 2 ) {
546					throw new ExprError( 'missing_operand', $this->names[$op] );
547				}
548				$right = array_pop( $stack );
549				$left = array_pop( $stack );
550				$stack[] = ( $left != $right ) ? 1 : 0;
551				break;
552			case EXPR_EXPONENT:
553				if ( count( $stack ) < 2 ) {
554					throw new ExprError( 'missing_operand', $this->names[$op] );
555				}
556				$right = array_pop( $stack );
557				$left = array_pop( $stack );
558				$stack[] = $left * pow( 10, $right );
559				break;
560			case EXPR_SINE:
561				if ( count( $stack ) < 1 ) {
562					throw new ExprError( 'missing_operand', $this->names[$op] );
563				}
564				$arg = array_pop( $stack );
565				$stack[] = sin( $arg );
566				break;
567			case EXPR_COSINE:
568				if ( count( $stack ) < 1 ) {
569					throw new ExprError( 'missing_operand', $this->names[$op] );
570				}
571				$arg = array_pop( $stack );
572				$stack[] = cos( $arg );
573				break;
574			case EXPR_TANGENS:
575				if ( count( $stack ) < 1 ) {
576					throw new ExprError( 'missing_operand', $this->names[$op] );
577				}
578				$arg = array_pop( $stack );
579				$stack[] = tan( $arg );
580				break;
581			case EXPR_ARCSINE:
582				if ( count( $stack ) < 1 ) {
583					throw new ExprError( 'missing_operand', $this->names[$op] );
584				}
585				$arg = array_pop( $stack );
586				if ( $arg < -1 || $arg > 1 ) {
587					throw new ExprError( 'invalid_argument', $this->names[$op] );
588				}
589				$stack[] = asin( $arg );
590				break;
591			case EXPR_ARCCOS:
592				if ( count( $stack ) < 1 ) {
593					throw new ExprError( 'missing_operand', $this->names[$op] );
594				}
595				$arg = array_pop( $stack );
596				if ( $arg < -1 || $arg > 1 ) {
597					throw new ExprError( 'invalid_argument', $this->names[$op] );
598				}
599				$stack[] = acos( $arg );
600				break;
601			case EXPR_ARCTAN:
602				if ( count( $stack ) < 1 ) {
603					throw new ExprError( 'missing_operand', $this->names[$op] );
604				}
605				$arg = array_pop( $stack );
606				$stack[] = atan( $arg );
607				break;
608			case EXPR_EXP:
609				if ( count( $stack ) < 1 ) {
610					throw new ExprError( 'missing_operand', $this->names[$op] );
611				}
612				$arg = array_pop( $stack );
613				$stack[] = exp( $arg );
614				break;
615			case EXPR_LN:
616				if ( count( $stack ) < 1 ) {
617					throw new ExprError( 'missing_operand', $this->names[$op] );
618				}
619				$arg = array_pop( $stack );
620				if ( $arg <= 0 ) {
621					throw new ExprError( 'invalid_argument_ln', $this->names[$op] );
622				}
623				$stack[] = log( $arg );
624				break;
625			case EXPR_ABS:
626				if ( count( $stack ) < 1 ) {
627					throw new ExprError( 'missing_operand', $this->names[$op] );
628				}
629				$arg = array_pop( $stack );
630				$stack[] = abs( $arg );
631				break;
632			case EXPR_FLOOR:
633				if ( count( $stack ) < 1 ) {
634					throw new ExprError( 'missing_operand', $this->names[$op] );
635				}
636				$arg = array_pop( $stack );
637				$stack[] = floor( $arg );
638				break;
639			case EXPR_TRUNC:
640				if ( count( $stack ) < 1 ) {
641					throw new ExprError( 'missing_operand', $this->names[$op] );
642				}
643				$arg = array_pop( $stack );
644				$stack[] = (int)$arg;
645				break;
646			case EXPR_CEIL:
647				if ( count( $stack ) < 1 ) {
648					throw new ExprError( 'missing_operand', $this->names[$op] );
649				}
650				$arg = array_pop( $stack );
651				$stack[] = ceil( $arg );
652				break;
653			case EXPR_POW:
654				if ( count( $stack ) < 2 ) {
655					throw new ExprError( 'missing_operand', $this->names[$op] );
656				}
657				$right = array_pop( $stack );
658				$left = array_pop( $stack );
659				$result = pow( $left, $right );
660				if ( $result === false ) {
661					throw new ExprError( 'division_by_zero', $this->names[$op] );
662				}
663				$stack[] = $result;
664				break;
665			case EXPR_SQRT:
666				if ( count( $stack ) < 1 ) {
667					throw new ExprError( 'missing_operand', $this->names[$op] );
668				}
669				$arg = array_pop( $stack );
670				$result = sqrt( $arg );
671				if ( is_nan( $result ) ) {
672					throw new ExprError( 'not_a_number', $this->names[$op] );
673				}
674				$stack[] = $result;
675				break;
676			default:
677				// Should be impossible to reach here.
678				// @codeCoverageIgnoreStart
679				throw new ExprError( 'unknown_error' );
680				// @codeCoverageIgnoreEnd
681		}
682	}
683}
684