Browse Source

First pass at expression parser/evaluator

master
Adam Pippin 3 years ago
parent
commit
6b3eaa6833
  1. 124
      app/Engine/Cfnpp/Expression/Expression.php
  2. 44
      app/Engine/Cfnpp/Expression/Parser.php
  3. 27
      app/Engine/Cfnpp/Expression/Token.php
  4. 45
      app/Engine/Cfnpp/Expression/TokenNumericLiteral.php
  5. 58
      app/Engine/Cfnpp/Expression/TokenOperator.php
  6. 61
      app/Engine/Cfnpp/Expression/TokenStringLiteral.php
  7. 46
      app/Engine/Cfnpp/Expression/TokenVariable.php

124
app/Engine/Cfnpp/Expression/Expression.php

@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
namespace App\Engine\Cfnpp\Expression;
class Expression
{
protected $tokens;
protected $stack;
protected $variables;
public function __construct(array $tokens)
{
$this->tokens = $tokens;
}
public function evaluate(array $variables = [])
{
$this->variables = $variables;
$this->stack = [];
$tokens = $this->tokens;
while (sizeof($tokens))
{
$token = array_shift($tokens);
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)
{
throw new \Exception('Expression did not evaluate down to a single value');
}
return $this->stack[0];
}
protected function evaluateTokenNumericLiteral(TokenNumericLiteral $token)
{
$this->push($token->getValue());
}
protected function evaluateTokenStringLiteral(TokenStringLiteral $token)
{
$this->push($token->getValue());
}
protected function evaluateTokenOperator(TokenOperator $token)
{
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;
}
}
protected function evaluateTokenVariable(TokenVariable $token)
{
$name = $token->getName();
if (!isset($this->variables[$name]))
{
throw new \Exception('Undefined variable: '.$name);
}
$this->push($this->variables[$name]);
}
protected function pop(int $offset = 0)
{
return array_splice($this->stack, -1 * ($offset + 1), 1)[0];
}
protected function push($value)
{
array_push($this->stack, $value);
}
}

44
app/Engine/Cfnpp/Expression/Parser.php

@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace App\Engine\Cfnpp\Expression;
class Parser
{
public const TOKEN_TYPES = [
TokenNumericLiteral::class,
TokenOperator::class,
TokenStringLiteral::class,
TokenVariable::class
];
public function parse(string $value): Expression
{
$original_value = $value;
$value = $value.' ';
$tokens = [];
while (strlen($value) > 0)
{
foreach (static::TOKEN_TYPES as $token_class)
{
if ($token_class::isToken($value))
{
$tokens[] = $token_class::getToken($value);
if (substr($value, 0, 1) != ' ')
{
throw new \Exception('Incompletely consumed token at offset '.(strlen($original_value) - strlen($value)).' in expression '.$original_value);
}
$value = substr($value, 1);
continue 2;
}
}
throw new \Exception('Unparseable value at offset '.(strlen($original_value) - strlen($value)).' in expression '.$original_value);
}
return new Expression($tokens);
}
}

27
app/Engine/Cfnpp/Expression/Token.php

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace App\Engine\Cfnpp\Expression;
abstract class Token
{
/**
* Check whether the stream passed in contains a valid token of this
* type.
*
* @param string $stream
* @return bool
*/
abstract public static function isToken(string $stream): bool;
/**
* Consume a token from the stream.
*
* Only valid if isToken is true.
*
* @param string $stream
* @return Token
*/
abstract public static function getToken(string &$stream): Token;
}

45
app/Engine/Cfnpp/Expression/TokenNumericLiteral.php

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace App\Engine\Cfnpp\Expression;
class TokenNumericLiteral extends Token
{
protected $value;
public function __construct($value)
{
$this->value = $value;
}
public function getValue()
{
return $this->value;
}
public static function isToken(string $stream): bool
{
return is_numeric($stream[0]);
}
public static function getToken(string &$stream): Token
{
$buffer = '';
for ($i = 0; $i < strlen($stream); $i++)
{
if (preg_match('/^[0-9]$/', $stream[$i]))
{
$buffer .= $stream[$i];
}
else
{
break;
}
}
$stream = substr($stream, strlen($buffer));
return new TokenNumericLiteral($buffer);
}
}

58
app/Engine/Cfnpp/Expression/TokenOperator.php

@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace App\Engine\Cfnpp\Expression;
class TokenOperator extends Token
{
public const OPERATORS = [
'eq',
'gt',
'gte',
'lt',
'lte',
'neq',
'and',
'or'
];
protected $operator;
public function __construct($operator)
{
$this->operator = $operator;
}
public function getOperator()
{
return $this->operator;
}
public static function isToken(string $stream): bool
{
foreach (static::OPERATORS as $operator)
{
if (strlen($stream) > strlen($operator) &&
substr($stream, 0, strlen($operator)) == $operator)
{
return true;
}
}
return false;
}
public static function getToken(string &$stream): Token
{
foreach (static::OPERATORS as $operator)
{
if (strlen($stream) > strlen($operator) &&
substr($stream, 0, strlen($operator)) == $operator)
{
$operator = substr($stream, 0, strlen($operator));
$stream = substr($stream, strlen($operator));
return new TokenOperator($operator);
}
}
}
}

61
app/Engine/Cfnpp/Expression/TokenStringLiteral.php

@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace App\Engine\Cfnpp\Expression;
class TokenStringLiteral extends Token
{
protected $value;
public function __construct($value)
{
$this->value = $value;
}
public function getValue()
{
return $this->value;
}
public static function isToken(string $stream): bool
{
return $stream[0] == '"';
}
public static function getToken(string &$stream): Token
{
$buffer = '';
$in_string = false;
$escaped = false;
for ($i = 0; $i < strlen($stream); $i++)
{
if ($escaped)
{
$buffer .= $stream[$i];
}
elseif ($stream[$i] == '"')
{
if ($in_string)
{
break;
}
$in_string = true;
}
elseif ($stream[$i] == '\\')
{
$escaped = true;
}
else
{
$buffer .= $stream[$i];
}
}
$stream = substr($stream, $i + 1);
return new TokenStringLiteral($buffer);
}
}

46
app/Engine/Cfnpp/Expression/TokenVariable.php

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace App\Engine\Cfnpp\Expression;
class TokenVariable extends Token
{
protected $name;
public function __construct($name)
{
$this->name = $name;
}
public function getName()
{
return $this->name;
}
public static function isToken(string $stream): bool
{
return preg_match('/^[A-Za-z]$/', $stream[0]);
}
public static function getToken(string &$stream): Token
{
$buffer = '';
$buffer = $stream[0];
for ($i = 1; $i < strlen($stream); $i++)
{
if (preg_match('/^[A-Za-z0-9]$/', $stream[$i]))
{
$buffer .= $stream[$i];
}
else
{
break;
}
}
$stream = substr($stream, strlen($buffer));
return new TokenVariable($buffer);
}
}
Loading…
Cancel
Save