From 6b3eaa683391360bfd559db80027bb307b590f2e Mon Sep 17 00:00:00 2001 From: Adam Pippin Date: Mon, 15 Feb 2021 00:05:12 -0800 Subject: [PATCH] First pass at expression parser/evaluator --- app/Engine/Cfnpp/Expression/Expression.php | 124 ++++++++++++++++++ app/Engine/Cfnpp/Expression/Parser.php | 44 +++++++ app/Engine/Cfnpp/Expression/Token.php | 27 ++++ .../Cfnpp/Expression/TokenNumericLiteral.php | 45 +++++++ app/Engine/Cfnpp/Expression/TokenOperator.php | 58 ++++++++ .../Cfnpp/Expression/TokenStringLiteral.php | 61 +++++++++ app/Engine/Cfnpp/Expression/TokenVariable.php | 46 +++++++ 7 files changed, 405 insertions(+) create mode 100644 app/Engine/Cfnpp/Expression/Expression.php create mode 100644 app/Engine/Cfnpp/Expression/Parser.php create mode 100644 app/Engine/Cfnpp/Expression/Token.php create mode 100644 app/Engine/Cfnpp/Expression/TokenNumericLiteral.php create mode 100644 app/Engine/Cfnpp/Expression/TokenOperator.php create mode 100644 app/Engine/Cfnpp/Expression/TokenStringLiteral.php create mode 100644 app/Engine/Cfnpp/Expression/TokenVariable.php diff --git a/app/Engine/Cfnpp/Expression/Expression.php b/app/Engine/Cfnpp/Expression/Expression.php new file mode 100644 index 0000000..dbd64e5 --- /dev/null +++ b/app/Engine/Cfnpp/Expression/Expression.php @@ -0,0 +1,124 @@ +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); + } +} diff --git a/app/Engine/Cfnpp/Expression/Parser.php b/app/Engine/Cfnpp/Expression/Parser.php new file mode 100644 index 0000000..f483438 --- /dev/null +++ b/app/Engine/Cfnpp/Expression/Parser.php @@ -0,0 +1,44 @@ + 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); + } +} diff --git a/app/Engine/Cfnpp/Expression/Token.php b/app/Engine/Cfnpp/Expression/Token.php new file mode 100644 index 0000000..e5f4c1e --- /dev/null +++ b/app/Engine/Cfnpp/Expression/Token.php @@ -0,0 +1,27 @@ +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); + } +} diff --git a/app/Engine/Cfnpp/Expression/TokenOperator.php b/app/Engine/Cfnpp/Expression/TokenOperator.php new file mode 100644 index 0000000..ca2c02e --- /dev/null +++ b/app/Engine/Cfnpp/Expression/TokenOperator.php @@ -0,0 +1,58 @@ +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); + } + } + } +} diff --git a/app/Engine/Cfnpp/Expression/TokenStringLiteral.php b/app/Engine/Cfnpp/Expression/TokenStringLiteral.php new file mode 100644 index 0000000..30bec17 --- /dev/null +++ b/app/Engine/Cfnpp/Expression/TokenStringLiteral.php @@ -0,0 +1,61 @@ +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); + } +} diff --git a/app/Engine/Cfnpp/Expression/TokenVariable.php b/app/Engine/Cfnpp/Expression/TokenVariable.php new file mode 100644 index 0000000..ea8cc1f --- /dev/null +++ b/app/Engine/Cfnpp/Expression/TokenVariable.php @@ -0,0 +1,46 @@ +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); + } +}