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