*/ class Expression { /** * List of Tokens in the expression. * @var Token[] */ protected $tokens; /** * Evaluation stack. * @var mixed[] */ protected $stack; /** * Variables that can be referenced in the expression. * @var array */ protected $variables; /** * Create a new expression. * * @param Token[] $tokens */ public function __construct(array $tokens) { $this->tokens = $tokens; } /** * Find all referenced variables so we can do proper ordering of variable * block evaluate. * * @return string[] */ public function getReferencedVariables(): array { $variables = []; foreach ($this->tokens as $token) { if ($token instanceof TokenVariable) { $variables[] = $token->getName(); } } return array_values(array_unique($variables)); } /** * Evaluate the tokens contained in this expression. * * @param array $variables variables that can be referenced * @return mixed if expression evaluated down to a single value a scalar, otherwise an array */ public function evaluate(array $variables = []) { $this->variables = $variables; $this->stack = []; $tokens = $this->tokens; while (sizeof($tokens)) { $token = array_shift($tokens); assert(isset($token)); $token_class = get_class($token); $token_name = substr($token_class, strrpos($token_class, '\\') + 1); $func = 'evaluate'.$token_name; if (method_exists($this, $func)) { $this->{$func}($token); } else { throw new \Exception('Unhandled expression token type: '.basename(get_class($token))); } } if (sizeof($this->stack) == 1) { return end($this->stack); } return $this->stack; } /** * Evaluate and mutate the stack given a numeric literal. * * @param TokenNumericLiteral $token * @return void */ protected function evaluateTokenNumericLiteral(TokenNumericLiteral $token): void { $this->push($token->getValue()); } /** * Evaluate and mutate the stack given a string literal. * * @param TokenStringLiteral $token * @return void */ protected function evaluateTokenStringLiteral(TokenStringLiteral $token): void { $this->push($token->getValue()); } /** * Evaluate and mutate the stack given a comparison operator. * * $this->pop() == $this->pop() is valid. * @suppress PhanPluginDuplicateExpressionBinaryOp * @param TokenOperator $token * @return void */ protected function evaluateTokenOperator(TokenOperator $token): void { switch ($token->getOperator()) { case 'eq': $this->push($this->pop() == $this->pop()); break; case 'neq': $this->push($this->pop() != $this->pop()); break; case 'gt': $this->push($this->pop(1) > $this->pop()); break; case 'gte': $this->push($this->pop(1) >= $this->pop()); break; case 'lt': $this->push($this->pop(1) < $this->pop()); break; case 'lte': $this->push($this->pop(1) <= $this->pop()); break; case 'and': $var1 = $this->pop(); $var2 = $this->pop(); $this->push($var1 && $var2); break; case 'or': $var1 = $this->pop(); $var2 = $this->pop(); $this->push($var1 || $var2); break; default: throw new \Exception('Unhandled comparison operator: '.$token->getOperator()); } } /** * Evaluate and mutate the stack given a function token. * * @param TokenFunction $token * @return void */ protected function evaluateTokenFunction(TokenFunction $token): void { switch ($token->getFunction()) { case 'concat': $this->push($this->pop(1).$this->pop()); break; case 'concat*': while (sizeof($this->stack) > 1) { $this->push($this->pop(1).$this->pop()); } break; default: throw new \Exception('Unhandled function: '.$token->getFunction()); } } /** * Evaluate and mutate the stack given a variable reference. * * @param TokenVariable $token * @return void */ protected function evaluateTokenVariable(TokenVariable $token) { $name = $token->getName(); if (!isset($this->variables[$name])) { throw new \Exception('Undefined variable: '.$name); } $this->push($this->variables[$name]); } /** * Pop an item off the stack. * * @param int $offset offset from the end of the stack to pop from * @return mixed */ protected function pop(int $offset = 0) { if (sizeof($this->stack) < $offset + 1) { throw new \Exception('Expression stack underflow!'); } return array_splice($this->stack, -1 * ($offset + 1), 1)[0]; } /** * Push an item onto the end of the stack. * * @param mixed $value * @return void */ protected function push($value): void { array_push($this->stack, $value); } }