Adam Pippin
3 years ago
7 changed files with 405 additions and 0 deletions
@ -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); |
|||
} |
|||
} |
@ -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); |
|||
} |
|||
} |
@ -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; |
|||
} |
@ -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); |
|||
} |
|||
} |
@ -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); |
|||
} |
|||
} |
|||
} |
|||
} |
@ -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); |
|||
} |
|||
} |
@ -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…
Reference in new issue