You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
195 lines
4.0 KiB
195 lines
4.0 KiB
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Engine\Cfnpp\Expression;
|
|
|
|
/**
|
|
* Expression that can be evaluated.
|
|
*
|
|
* @author Adam Pippin <hello@adampippin.ca>
|
|
*/
|
|
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<string,mixed>
|
|
*/
|
|
protected $variables;
|
|
|
|
/**
|
|
* Create a new expression.
|
|
*
|
|
* @param Token[] $tokens
|
|
*/
|
|
public function __construct(array $tokens)
|
|
{
|
|
$this->tokens = $tokens;
|
|
}
|
|
|
|
/**
|
|
* Evaluate the tokens contained in this expression.
|
|
*
|
|
* @param array<string,mixed> $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));
|
|
|
|
if ($token instanceof TokenNumericLiteral)
|
|
{
|
|
$this->evaluateTokenNumericLiteral($token);
|
|
}
|
|
elseif ($token instanceof TokenStringLiteral)
|
|
{
|
|
$this->evaluateTokenStringLiteral($token);
|
|
}
|
|
elseif ($token instanceof TokenOperator)
|
|
{
|
|
$this->evaluateTokenOperator($token);
|
|
}
|
|
elseif ($token instanceof TokenVariable)
|
|
{
|
|
$this->evaluateTokenVariable($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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
}
|
|
|