Compare commits

...

18 Commits

Author SHA1 Message Date
Adam Pippin 326e856a2b Cleanup 3 years ago
Adam Pippin 0265db1946 Rework expression parser to not solve on creation; re-add dependency solving for variables in expressions in variables block 3 years ago
Adam Pippin 6f90b5d222 Bugfix: Fix processing order for functions -- deepest nodes first 3 years ago
Adam Pippin 8dd7b8d1f0 Re-add !expr function 3 years ago
Adam Pippin 4294c5b007 Add concat/select operators 3 years ago
Adam Pippin 6ddf1531af Add boolean literals to expression parser 3 years ago
Adam Pippin 706242074d Allow !replace to accept a scalar 3 years ago
Adam Pippin 9e5adfb9ce Bugfix: !if that creates cfn conditions properly replaces parent node 3 years ago
Adam Pippin a3fe71526d Bugfix: generating Fn::If without false caused error 3 years ago
Adam Pippin 9a99fc315b Bugfix: expression binary operators were dropping children 3 years ago
Adam Pippin b38a37d048 Update README.md 3 years ago
Adam Pippin 2b619d767b Remove old expression parser 3 years ago
Adam Pippin 41b26006ac Cleanup + comments 3 years ago
Adam Pippin 22aa9e068f Update fn::if to use new expression parser and generate conditions where appropriate 3 years ago
Adam Pippin f58cd9129d Add an `addCondition` method to compiler for calling from functions 3 years ago
Adam Pippin 27774c84c6 Expression bug fix replacing variables; add string literal 3 years ago
Adam Pippin 5d0afd4984 Reimplement expression parser 3 years ago
Adam Pippin 01b036b284 Move cfnpp code out from under engine namespace 3 years ago
  1. 4
      README.md
  2. 130
      app/Cfnpp/Compiler.php
  3. 372
      app/Cfnpp/Expression/Expression.php
  4. 22
      app/Cfnpp/Expression/ICloudformationNative.php
  5. 40
      app/Cfnpp/Expression/Token.php
  6. 75
      app/Cfnpp/Expression/Token/BooleanLiteral.php
  7. 42
      app/Cfnpp/Expression/Token/NumericLiteral.php
  8. 211
      app/Cfnpp/Expression/Token/OperatorBinary.php
  9. 121
      app/Cfnpp/Expression/Token/OperatorUnary.php
  10. 91
      app/Cfnpp/Expression/Token/Parameter.php
  11. 40
      app/Cfnpp/Expression/Token/StringLiteral.php
  12. 43
      app/Cfnpp/Expression/Token/Variable.php
  13. 12
      app/Cfnpp/Expression/TokenBinary.php
  14. 12
      app/Cfnpp/Expression/TokenLiteral.php
  15. 12
      app/Cfnpp/Expression/TokenUnary.php
  16. 105
      app/Cfnpp/Functions.php
  17. 2
      app/Cfnpp/Options.php
  18. 4
      app/Commands/Stack/Compile.php
  19. 231
      app/Engine/Cfnpp/Expression/Expression.php
  20. 61
      app/Engine/Cfnpp/Expression/Parser.php
  21. 32
      app/Engine/Cfnpp/Expression/Token.php
  22. 90
      app/Engine/Cfnpp/Expression/TokenFunction.php
  23. 96
      app/Engine/Cfnpp/Expression/TokenOperator.php
  24. 212
      app/Util/GraphNode.php
  25. 73
      app/Util/Stack.php

4
README.md

@ -9,12 +9,16 @@ For more information, see the [wiki](/nucleardog/cfnpp/wiki).
* Provision rendered templates directly to CloudFormation * Provision rendered templates directly to CloudFormation
* More flow control (e.g., foreach, etc) * More flow control (e.g., foreach, etc)
* Some sort of macro system would be good. * Some sort of macro system would be good.
* Rewrite DOM stuff to use Util\GraphNode class
* Figure out how to generate useful conditions and not duplicate them
## License ## License
No license applied yet. No permission is granted to use, redestribute, modify, or No license applied yet. No permission is granted to use, redestribute, modify, or
otherwise do anything with this software. otherwise do anything with this software.
I want to push this a little further along before opening it up to use/contributions/etc.
## Author ## Author
* Adam Pippin <hello@adampippin.ca> * Adam Pippin <hello@adampippin.ca>

130
app/Engine/Cfnpp/Compiler.php → app/Cfnpp/Compiler.php

@ -2,7 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
namespace App\Engine\Cfnpp; namespace App\Cfnpp;
use App\Dom\Document; use App\Dom\Document;
use App\Dom\Node; use App\Dom\Node;
@ -41,6 +41,13 @@ class Compiler implements \App\Engine\ICompile
*/ */
protected $merge_functions; protected $merge_functions;
/**
* Stores current state of the document so it can be mutated mid-pass.
*
* @var ?Document
*/
protected $document;
/** /**
* Register a function that can be called by the document. * Register a function that can be called by the document.
* *
@ -66,6 +73,28 @@ class Compiler implements \App\Engine\ICompile
$this->merge_functions[$name] = $callback; $this->merge_functions[$name] = $callback;
} }
/**
* Add a condition to the document currently being processed.
*
* The only appropriate place to call this is from the document functions
*
* @param string $name name of condition to create
* @param Node $node
* @return void
*/
public function addCondition(string $name, Node $node): void
{
$conditions = $this->document->getChildByName('Conditions');
if (!isset($conditions))
{
$conditions = new Node($this->document, 'Conditions');
$this->document->addChild($conditions);
}
$node->setName($name);
$conditions->addChild($node);
}
/** /**
* Compile a set of documents and return the result. * Compile a set of documents and return the result.
* *
@ -81,7 +110,7 @@ class Compiler implements \App\Engine\ICompile
} }
// Initialize state // Initialize state
$document = null; $this->document = null;
$this->functions = []; $this->functions = [];
$this->merge_functions = []; $this->merge_functions = [];
@ -89,21 +118,11 @@ class Compiler implements \App\Engine\ICompile
$cfnpp_functions = new Functions($this, $options); $cfnpp_functions = new Functions($this, $options);
$cfnpp_functions->register($this); $cfnpp_functions->register($this);
$document = $this->pass_0($documents, $options); $this->document = $document = $this->pass_0($documents, $options);
$this->pass_1($document, $options); $this->pass_1($document, $options);
$this->pass_2($document, $options); $this->pass_2($document, $options);
return $document; return $this->document;
// Process each passed document
/*
foreach ($documents as $next_document)
{
$this->runMergeFunctions($document, $next_document);
$this->merge($document, $next_document);
$this->runFunctions($document);
}
*/
} }
/** /**
@ -171,8 +190,8 @@ class Compiler implements \App\Engine\ICompile
} }
/** /**
* Compiler Pass 1 - Grab all variables, build dependency graph of * Compiler Pass 1 - Grab all variables and parameters, build dependency
* dependencies between variable values, resolve variables values. Then * graph of dependencies between values, resolve values. Then
* set them on $options to include them in program state. * set them on $options to include them in program state.
* *
* @param Document $document * @param Document $document
@ -182,32 +201,61 @@ class Compiler implements \App\Engine\ICompile
protected function pass_1(Document $document, IOptions $options): void protected function pass_1(Document $document, IOptions $options): void
{ {
$variables_node = $document->getChildByPath('cfnpp.variables'); $variables_node = $document->getChildByPath('cfnpp.variables');
// If there are no variables, we're done here. $parameters_node = $document->getChildByPath('cfnpp.parameters');
if (!isset($variables_node)) // If there are no variables or parameters, we're done here.
if (!isset($variables_node) && !isset($parameters_node))
{ {
return; return;
} }
$variables_raw = []; $nodes = [];
$graph = new DependencyGraph(); $graph = new DependencyGraph();
foreach ($variables_node as $variable_node) if (isset($parameters_node))
{ {
$variables_raw[$variable_node->getName()] = $variable_node; foreach ($parameters_node as $parameter_node)
$graph->add($variable_node->getName(), $this->pass_1_getVariableDependencies($variable_node)); {
$nodes[$parameter_node->getName()] = $parameter_node;
$graph->add($parameter_node->getName(), $this->pass_1_getDependencies($parameter_node, $options));
}
}
if (isset($variables_node))
{
foreach ($variables_node as $variable_node)
{
if (isset($nodes[$variable_node->getName()]))
{
throw new \Exception('Variables and parameters cannot share the same name.');
}
$nodes[$variable_node->getName()] = $variable_node;
$graph->add($variable_node->getName(), $this->pass_1_getDependencies($variable_node, $options));
}
} }
$variables_ordered = $graph->solve(); $nodes_ordered = $graph->solve();
foreach ($variables_ordered as $variable) foreach ($nodes_ordered as $node_name)
{ {
$this->runFunctions($variables_raw[$variable]); $this->runFunctions($nodes[$node_name]);
$variable_node = $variables_node->getChildByName($variable);
// By all accounts this _should_ still exist, but this accounts for the $type = $nodes[$node_name]->getParent()->getName();
// case where, say, someone had used !unset on a variable for some reason.
if (isset($variable_node)) if ($type == 'parameters')
{
$parameter_node = $parameters_node->getChildByName($node_name);
if (isset($parameter_node))
{
$options->setParameter($parameter_node->getName(), Node::toPhp($parameter_node));
}
}
elseif ($type == 'variables')
{ {
$options->setVariable($variable, Node::toPhp($variable_node)); $variable_node = $variables_node->getChildByName($node_name);
if (isset($variable_node))
{
$options->setVariable($variable_node->getName(), Node::toPhp($variable_node));
}
} }
} }
} }
@ -220,7 +268,7 @@ class Compiler implements \App\Engine\ICompile
* @param Node $node * @param Node $node
* @return string[] * @return string[]
*/ */
protected function pass_1_getVariableDependencies(Node $node): array protected function pass_1_getDependencies(Node $node, IOptions $options): array
{ {
$stack = [$node]; $stack = [$node];
@ -240,11 +288,15 @@ class Compiler implements \App\Engine\ICompile
{ {
$variables[] = $node->getValue(); $variables[] = $node->getValue();
} }
elseif ($node instanceof NodeFunctionValue &&
$node->getName() == 'param')
{
$variables[] = $node->getValue();
}
elseif ($node instanceof NodeFunctionValue && elseif ($node instanceof NodeFunctionValue &&
$node->getName() == 'expr') $node->getName() == 'expr')
{ {
$parser = new \App\Engine\Cfnpp\Expression\Parser(); $expression = new \App\Cfnpp\Expression\Expression($node->getValue());
$expression = $parser->parse($node->getValue());
$variables = array_merge($variables, $expression->getReferencedVariables()); $variables = array_merge($variables, $expression->getReferencedVariables());
} }
} }
@ -391,6 +443,12 @@ class Compiler implements \App\Engine\ICompile
*/ */
protected function runFunctions(Node $node): void protected function runFunctions(Node $node): void
{ {
$children = $node->getChildren();
foreach ($children as $child)
{
$this->runFunctions($child);
}
if ($node->isFunctionParent() && isset($this->functions[$node[0]->getName()])) if ($node->isFunctionParent() && isset($this->functions[$node[0]->getName()]))
{ {
$function_node = $node[0]; $function_node = $node[0];
@ -408,12 +466,6 @@ class Compiler implements \App\Engine\ICompile
return; return;
} }
} }
$children = $node->getChildren();
foreach ($children as $child)
{
$this->runFunctions($child);
}
} }
/** /**

372
app/Cfnpp/Expression/Expression.php

@ -0,0 +1,372 @@
<?php
declare(strict_types=1);
namespace App\Cfnpp\Expression;
use App\Util\GraphNode;
use App\Util\Stack;
/**
* Represents an expression and handles tokenizing, parsing, and executing
* expressions.
*
* @author Adam Pippin <hello@adampippin.ca>
*/
class Expression
{
/**
* References to all Token implementations this class can handle.
* @var string[]
*/
public const TOKEN_TYPES = [
Token\BooleanLiteral::class,
Token\NumericLiteral::class,
Token\OperatorUnary::class,
Token\OperatorBinary::class,
Token\StringLiteral::class,
Token\Variable::class
];
/**
* Solved expression this represents.
* @var GraphNode[]
*/
protected $nodes;
/**
* Referencing to our compilation state for accessing variables/parameters.
* @var \App\Engine\IOptions
*/
protected $options;
/**
* Create a new expression.
*
* Expression is tokenized, parsed, and solved on creation.
*
* @param string $expression
*/
public function __construct(string $expression)
{
$tokens = static::tokenize($expression);
$this->nodes = static::parse($tokens);
}
/**
* Solve this expression down to the minimal set of nodes we can
*
* @param \App\Engine\IOptions $options
* @return void
*/
public function solve(\App\Engine\IOptions $options): void
{
$this->nodes = static::_solve($this->nodes, $options->getVariables(), array_keys($options->getParameters()));
}
/**
* Check whether this expression is 'complete'.
*
* Complete is defined as having been solved down to a single value, or array
* of single values none of which are still tokens. That is, we have a set of
* computed scalars and no unresolved var/param references.
*
* @return bool
*/
public function isComplete(): bool
{
// I think this works? If we resolved down to a flat set of values and
// all are scalar, then we're done?
$complete = true;
foreach ($this->nodes as $node)
{
if ($node->getValue() instanceof Token ||
$node->hasChildren())
{
$complete = false;
break;
}
}
return $complete;
}
/**
* Check how many values are contained in the solution.
*
* @return int
*/
public function count(): int
{
return sizeof($this->nodes);
}
/**
* Assuming the solution contains only a single value, fetch it.
*
* @return mixed
*/
public function getValue()
{
return $this->nodes[0]->getValue();
}
/**
* Fetch the solution as an array.
*
* Only valid if the solution is complete
*
* @return mixed[]
*/
public function toArray(): array
{
return static::unwrap($this->nodes);
}
/**
* Convert an incomplete solution into a CloudFormation condition using
* CloudFormation intrinsic functions.
*
* @return mixed[]
*/
public function toCloudformation(): array
{
return static::cloudformation($this->nodes);
}
/**
* Examine the expression to determine which variables are referenced so we
* can figure out the dependencies between them
*
* @return string[] names of variables referenced
*/
public function getReferencedVariables(): array
{
$variables = [];
foreach ($this->nodes as $node)
{
$node->walk(function(GraphNode $node) use (&$variables): void {
if ($node->getValue() instanceof Token\Variable)
{
$variables[] = $node->getValue()->getName();
}
});
}
return array_values(array_unique($variables));
}
/**
* Convert an expression string into a series of tokens.
*
* @param string $expression
* @return Token[]
*/
protected static function tokenize(string $expression): array
{
$tokens = [];
while (strlen($expression) > 0)
{
foreach (static::TOKEN_TYPES as $token_class)
{
if ($token_class::isToken($expression))
{
$tokens[] = $token_class::getToken($expression);
if (strlen($expression) > 0 && substr($expression, 0, 1) != ' ')
{
throw new \Exception('incompletely consumed token');
}
$expression = substr($expression, 1);
continue 2;
}
}
throw new \Exception('unparseable value');
}
return $tokens;
}
/**
* Build a tree out of a series of tokens.
*
* @param Token[] $tokens
* @return GraphNode[]
*/
protected static function parse(array $tokens): array
{
$stack = new Stack();
foreach ($tokens as $token)
{
if ($token instanceof TokenLiteral)
{
$stack->push(new GraphNode($token));
}
elseif ($token instanceof TokenUnary)
{
$node = new GraphNode($token);
$node->appendChild($stack->pop());
$stack->push($node);
}
elseif ($token instanceof TokenBinary)
{
$node = new GraphNode($token);
$node->appendChild($stack->pop());
// @phan-suppress-next-line PhanPluginDuplicateAdjacentStatement
$node->appendChild($stack->pop());
$stack->push($node);
}
}
return $stack->get();
}
/**
* Solve a tree of tokens by inserting all variable values and wherever possible
* resolving all functions.
*
* @param GraphNode[] $nodes parsed tree
* @param array<string,mixed> $variables variable names and values
* @param string[] $parameters parameter names
* @return GraphNode[]
*/
protected static function _solve(array $nodes, array $variables = [], array $parameters = []): array
{
$root = new GraphNode();
foreach ($nodes as $node)
{
$root->appendChild($node);
}
static::fillVariables($root->getChildren(), $variables, $parameters);
static::collapse($root->getChildren());
return $root->getChildren();
}
/**
* Convert a tree of of nodes into something that could be a valid CloudFormation
* condition.
*
* @param GraphNode[] $nodes
* @throws \Exception if the remaining nodes contain functions that cannot be expressed in cloudformation
* @return mixed[]
*/
protected static function cloudformation(array $nodes): array
{
foreach ($nodes as $node)
{
$node->walk(static function(GraphNode $node): void {
if (is_scalar($node->getValue()))
{
return;
}
if (!($node->getValue() instanceof ICloudformationNative))
{
throw new \Exception('Token '.basename(get_class($node->getValue())).' is not natively supported by CloudFormation.');
}
$node->setValue($node->getValue()->toCloudformation($node->getChildren()));
$node->clearChildren();
});
}
if (sizeof($nodes) > 1)
{
throw new \Exception('Expression cannot be converted to CloudFormation -- contains multiple nodes');
}
return $nodes[0]->getValue();
}
/**
* Fill all variable values by replacing variable tokens with their actual values.
*
* @param GraphNode[] $nodes
* @param array<string,mixed> $variables variable values
* @param string[] $parameters parameter names
* @throws \Exception if a reference is made to an undefined variable
* @return void
*/
protected static function fillVariables(array $nodes, array $variables, array $parameters): void
{
foreach ($nodes as $node)
{
$node->walk(static function(GraphNode $node) use ($variables, $parameters): void {
if ($node->getValue() instanceof Token\Variable)
{
$var_name = $node->getValue()->getName();
if (in_array($var_name, $parameters))
{
$node->getParent()->replaceChild($node, new GraphNode(new Token\Parameter($var_name)));
}
elseif (!isset($variables[$var_name]))
{
throw new \Exception('Undefined variable: '.$var_name);
}
else
{
$node->setValue($variables[$var_name]);
}
}
});
}
}
/**
* 'collapse' a node tree by executing nodes.
*
* @param GraphNode[] $nodes
* @return void
*/
protected static function collapse(array $nodes): void
{
foreach ($nodes as $node)
{
$node->walk(static function(GraphNode $node): void {
if ($node->getValue() instanceof Token\Parameter)
{
return;
}
if ($node->getValue() instanceof Token)
{
$result = $node->getValue()->execute($node->getChildren());
if (is_scalar($result))
{
$node->setValue($result);
$node->clearChildren();
}
elseif ($result instanceof Token)
{
$node->setValue($result);
$node->clearChildren();
}
elseif ($result instanceof GraphNode)
{
$node->getParent()->replaceChild($node, $result);
}
}
});
}
}
/**
* Unwrap an array of nodes by returning their values.
*
* Not valid if any nodes still contain children, as the assumption is that
* those nodes are unresolved.
*
* @param GraphNode[] $nodes
* @return mixed[]
*/
protected static function unwrap(array $nodes)
{
return array_map(/** @return mixed */ static function(GraphNode $node) {
if ($node->hasChildren())
{
throw new \Exception('Cannot unwrap node: still has children');
}
return $node->getValue();
}, $nodes);
}
}

22
app/Cfnpp/Expression/ICloudformationNative.php

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace App\Cfnpp\Expression;
/**
* Token type that supports being converted directly to a CloudFormation intrinsic.
*
* @author Adam Pippin <hello@adampippin.ca>
*/
interface ICloudformationNative
{
/**
* Convert this token to a CloudFormation intrinsic, formatted as a simple
* PHP array.
*
* @param ?\App\Util\GraphNode[] $arguments requested token parameters
* @return mixed[]
*/
public function toCloudformation(?array $arguments = null): array;
}

40
app/Cfnpp/Expression/Token.php

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace App\Cfnpp\Expression;
/**
* Token parsed out of an expression.
*
* @author Adam Pippin <hello@adampippin.ca>
*/
abstract class Token
{
/**
* Determine whether the passed stream contains an instance of this token.
*
* @param string $stream
* @return bool
*/
abstract public static function isToken(string $stream): bool;
/**
* Consume a token from the passed stream.
*
* This method is expected to modify the stream to remove all
* consumed characters
*
* @param string $stream
* @return Token
*/
abstract public static function getToken(string &$stream): Token;
/**
* Execute the token given the requested parameters.
*
* @param ?\App\Util\GraphNode[] $arguments
* @return \App\Util\GraphNode|Token|scalar|null
*/
abstract public function execute(?array $arguments = null);
}

75
app/Cfnpp/Expression/Token/BooleanLiteral.php

@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace App\Cfnpp\Expression\Token;
use App\Cfnpp\Expression\Token;
use App\Cfnpp\Expression\TokenLiteral;
/**
* A boolean literal (true/false).
*
* @author Adam Pippin <hello@adampippin.ca>
*/
class BooleanLiteral extends TokenLiteral
{
/**
* Value of this literal.
* @var bool
*/
protected $value;
/**
* New boolean literal.
*
* @param bool $value
*/
public function __construct(bool $value)
{
$this->value = $value;
}
/**
* Get the value of this literal.
*
* @return bool
*/
public function getValue(): bool
{
return $this->value;
}
public static function isToken(string $stream): bool
{
return
(strlen($stream) >= 4 && strtolower(substr($stream, 0, 4)) == 'true') ||
(strlen($stream) >= 5 && strtolower(substr($stream, 0, 5)) == 'false');
}
public static function getToken(string &$stream): Token
{
if (strlen($stream) >= 4 && strtolower(substr($stream, 0, 4)) == 'true')
{
$stream = substr($stream, 4);
return new BooleanLiteral(true);
}
elseif (strlen($stream) >= 5 && strtolower(substr($stream, 0, 5)) == 'false')
{
$stream = substr($stream, 5);
return new BooleanLiteral(false);
}
throw new \Exception('Unparseable boolean');
}
/**
* Get the value of this token.
*
* @param ?\App\Util\GraphNode[] $arguments
* @return \App\Util\GraphNode|Token|scalar|null
*/
public function execute(?array $arguments = null)
{
return $this->getValue();
}
}

42
app/Engine/Cfnpp/Expression/TokenNumericLiteral.php → app/Cfnpp/Expression/Token/NumericLiteral.php

@ -2,23 +2,26 @@
declare(strict_types=1); declare(strict_types=1);
namespace App\Engine\Cfnpp\Expression; namespace App\Cfnpp\Expression\Token;
use App\Cfnpp\Expression\Token;
use App\Cfnpp\Expression\TokenLiteral;
/** /**
* Token representing a numeric (integer or float) literal. * A number literal.
* *
* @author Adam Pippin <hello@adampippin.ca> * @author Adam Pippin <hello@adampippin.ca>
*/ */
class TokenNumericLiteral extends Token class NumericLiteral extends TokenLiteral
{ {
/** /**
* Numeric value this token represents. * Value of this literal.
* @var int|float * @var int|float
*/ */
protected $value; protected $value;
/** /**
* Create a new numeric literal. * New number literal.
* *
* @param int|float $value * @param int|float $value
*/ */
@ -28,7 +31,7 @@ class TokenNumericLiteral extends Token
} }
/** /**
* Get the value this token represents. * Get the value of this literal.
* *
* @return int|float * @return int|float
*/ */
@ -37,25 +40,11 @@ class TokenNumericLiteral extends Token
return $this->value; return $this->value;
} }
/**
* Determine whether a numeric token can be parsed from a stream.
*
* @param string $stream
* @return bool
*/
public static function isToken(string $stream): bool public static function isToken(string $stream): bool
{ {
return is_numeric($stream[0]); return is_numeric($stream[0]);
} }
/**
* Parse a numeric token from a stream.
*
* Returns token, and modifies stream to remove all consumed characters.
*
* @param string $stream
* @return Token
*/
public static function getToken(string &$stream): Token public static function getToken(string &$stream): Token
{ {
$buffer = ''; $buffer = '';
@ -82,6 +71,17 @@ class TokenNumericLiteral extends Token
$buffer = (int)$buffer; $buffer = (int)$buffer;
} }
return new TokenNumericLiteral($buffer); return new NumericLiteral($buffer);
}
/**
* Get the value of this token.
*
* @param ?\App\Util\GraphNode[] $arguments
* @return \App\Util\GraphNode|Token|scalar|null
*/
public function execute(?array $arguments = null)
{
return $this->getValue();
} }
} }

211
app/Cfnpp/Expression/Token/OperatorBinary.php

@ -0,0 +1,211 @@
<?php
declare(strict_types=1);
namespace App\Cfnpp\Expression\Token;
use App\Cfnpp\Expression\Token;
use App\Cfnpp\Expression\TokenBinary;
use App\Cfnpp\Expression\ICloudformationNative;
/**
* Basic comparison operators that take two parameters.
*
* @author Adam Pippin <hello@adampippin.ca>
*/
class OperatorBinary extends TokenBinary implements ICloudformationNative
{
/**
* List of valid operators.
* @var string[]
*/
public const OPERATORS = [
'and',
'or',
'eq',
'concat',
'select'
];
/**
* The operator this instance represents.
* @var string
*/
protected $operator;
/**
* New binary operator.
*
* @param string $operator
*/
public function __construct(string $operator)
{
$this->operator = $operator;
}
/**
* Get the operator this instance represents.
*
* @return string
*/
public function getOperator(): string
{
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 OperatorBinary($operator);
}
}
throw new \Exception('Could not parse OperatorBinary');
}
/**
* Execute the operator.
*
* Suppressing accesses to arguments since this is guaranteed valid
* unless there are bugs in Expression.
* @suppress PhanTypeArraySuspiciousNullable
* @param ?\App\Util\GraphNode[] $arguments
* @return \App\Util\GraphNode|Token|scalar|null
*/
public function execute(?array $arguments = null)
{
$value1 = $arguments[0]->getValue();
$value2 = $arguments[1]->getValue();
switch ($this->getOperator())
{
case 'and':
if (is_scalar($value1) && is_scalar($value2))
{
return $value1 && $value2;
}
elseif (is_scalar($value1))
{
return $value1 ? $arguments[1] : false;
}
elseif (is_scalar($value2))
{
return $value2 ? $arguments[0] : false;
}
elseif ($value1 instanceof Parameter && $value2 instanceof Parameter && $value1->getName() == $value2->getName())
{
return $value1;
}
return null;
case 'or':
if (is_scalar($value1) && is_scalar($value2))
{
return $value1 || $value2;
}
elseif (is_scalar($value1))
{
return $value1 ? true : $arguments[1];
}
elseif (is_scalar($value2))
{
return $value2 ? true : $arguments[0];
}
elseif ($value1 instanceof Parameter && $value2 instanceof Parameter && $value1->getName() == $value2->getName())
{
return $value1;
}
return null;
case 'eq':
if (is_scalar($value1) && is_scalar($value2))
{
return $arguments[0] == $arguments[1];
}
elseif ($value1 instanceof Parameter && $value2 instanceof Parameter && $value1->getName() == $value2->getName())
{
return true;
}
return null;
case 'concat':
if (is_scalar($value1) && is_scalar($value2))
{
return $value2.$value1;
}
elseif ($value1 instanceof Parameter && is_scalar($value2) && empty($value2))
{
return $value1;
}
elseif ($value2 instanceof Parameter && is_scalar($value1) && empty($value1))
{
return $value2;
}
return null;
case 'select':
if (is_scalar($value1) && is_array($value2))
{
return $value2[$value1];
}
return null;
default:
throw new \Exception('Missing implementation for binary operator: '.$this->getOperator());
}
}
/**
* Convert this token into a CloudFormation intrinsic.
*
* Suppressing accesses to arguments since this is guaranteed valid
* unless there are bugs in Expression.
* @suppress PhanTypeArraySuspiciousNullable
* @param ?\App\Util\GraphNode[] $arguments
* @return mixed[]
*/
public function toCloudformation(?array $arguments = null): array
{
$value1 = $arguments[0]->getValue();
$value2 = $arguments[1]->getValue();
switch ($this->getOperator())
{
case 'and':
return ['Fn::And' => [$value1, $value2]];
case 'or':
return ['Fn::Or' => [$value1, $value2]];
case 'eq':
return ['Fn::Equals' => [$value1, $value2]];
case 'concat':
return ['Fn::Join' => ['', [$value2, $value1]]];
case 'select':
return ['Fn::Select' => [$value1, $value2]];
default:
throw new \Exception('Operator cannot be applied to parameters: '.$this->getOperator());
}
}
}

121
app/Cfnpp/Expression/Token/OperatorUnary.php

@ -0,0 +1,121 @@
<?php
declare(strict_types=1);
namespace App\Cfnpp\Expression\Token;
use App\Cfnpp\Expression\Token;
use App\Cfnpp\Expression\TokenUnary;
use App\Cfnpp\Expression\ICloudformationNative;
/**
* Basic operators that take one parameter.
*
* @author Adam Pippin <hello@adampippin.ca>
*/
class OperatorUnary extends TokenUnary implements ICloudformationNative
{
/**
* List of valid operators.
* @var string[]
*/
public const OPERATORS = [
'not'
];
/**
* The operator this instance represents.
* @var string
*/
protected $operator;
/**
* New unary operator.
*
* @param string $operator
*/
public function __construct($operator)
{
$this->operator = $operator;
}
/**
* Get the operator this instance represents.
*
* @return string
*/
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 OperatorUnary($operator);
}
}
throw new \Exception('Could not parse OperatorUnary');
}
/**
* Execute the operator.
*
* Suppressing accesses to arguments since this is guaranteed valid
* unless there are bugs in Expression.
* @suppress PhanTypeArraySuspiciousNullable
* @param ?\App\Util\GraphNode[] $arguments
* @return \App\Util\GraphNode|Token|scalar|null
*/
public function execute(?array $arguments = null)
{
$value = $arguments[0]->getValue();
switch ($this->getOperator())
{
case 'not':
return is_scalar($value) ? !$value : null;
default:
throw new \Exception('Missing implementation for unary operator: '.$this->getOperator());
}
}
/**
* Convert this token into a CloudFormation intrinsic.
*
* Suppressing accesses to arguments since this is guaranteed valid
* unless there are bugs in Expression.
* @suppress PhanTypeArraySuspiciousNullable
* @param ?\App\Util\GraphNode[] $arguments
* @return mixed[]
*/
public function toCloudformation(?array $arguments = null): array
{
$value = $arguments[0]->getValue();
switch ($this->getOperator())
{
case 'not':
return ['Fn::Not' => [$value]];
default:
throw new \Exception('Missing implementation for unary operator: '.$this->getOperator());
}
}
}

91
app/Cfnpp/Expression/Token/Parameter.php

@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
namespace App\Cfnpp\Expression\Token;
use App\Cfnpp\Expression\Token;
use App\Cfnpp\Expression\TokenLiteral;
use App\Cfnpp\Expression\ICloudformationNative;
/**
* Represents a reference to a CloudFormation parameter.
*
* @author Adam Pippin <hello@adampippin.ca>
*/
class Parameter extends TokenLiteral implements ICloudformationNative
{
/**
* Name of the referenced parameter.
* @var string
*/
protected $name;
/**
* New parameter.
* @string $name
* @param string $name
*/
public function __construct(string $name)
{
$this->name = $name;
}
/**
* Get the name of the parameter.
*
* @return string
*/
public function getName(): string
{
return $this->name;
}
public static function isToken(string $stream): bool
{
return (bool)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 Parameter($buffer);
}
/**
* Get the value of this token.
*
* @param ?\App\Util\GraphNode[] $arguments
* @return \App\Util\GraphNode|Token|scalar|null
*/
public function execute(?array $arguments = null)
{
throw new \Exception('Unreplaced parameter');
}
/**
* Return the cloudformation representation of a reference to a parameter.
*
* @param ?\App\Util\GraphNode[] $arguments
* @return mixed[]
*/
public function toCloudformation(?array $arguments = null): array
{
return ['Ref' => $this->getName()];
}
}

40
app/Engine/Cfnpp/Expression/TokenStringLiteral.php → app/Cfnpp/Expression/Token/StringLiteral.php

@ -2,14 +2,17 @@
declare(strict_types=1); declare(strict_types=1);
namespace App\Engine\Cfnpp\Expression; namespace App\Cfnpp\Expression\Token;
use App\Cfnpp\Expression\Token;
use App\Cfnpp\Expression\TokenLiteral;
/** /**
* Token representing a string literal. * A string literal.
* *
* @author Adam Pippin <hello@adampippin.ca> * @author Adam Pippin <hello@adampippin.ca>
*/ */
class TokenStringLiteral extends Token class StringLiteral extends TokenLiteral
{ {
/** /**
* Value of the string literal. * Value of the string literal.
@ -18,7 +21,7 @@ class TokenStringLiteral extends Token
protected $value; protected $value;
/** /**
* Create a new string literal token. * New string literal.
* *
* @param string $value * @param string $value
*/ */
@ -28,7 +31,7 @@ class TokenStringLiteral extends Token
} }
/** /**
* Get the value of the string literal. * Get the value of this literal.
* *
* @return string * @return string
*/ */
@ -37,25 +40,11 @@ class TokenStringLiteral extends Token
return $this->value; return $this->value;
} }
/**
* Determine whether a string token can be parsed from a stream.
*
* @param string $stream
* @return bool
*/
public static function isToken(string $stream): bool public static function isToken(string $stream): bool
{ {
return $stream[0] == '"'; return $stream[0] == '"';
} }
/**
* Parse a string literal token from a stream.
*
* Returns token, and modifies stream to remove all consumed characters
*
* @param string $stream
* @return Token
*/
public static function getToken(string &$stream): Token public static function getToken(string &$stream): Token
{ {
$buffer = ''; $buffer = '';
@ -90,6 +79,17 @@ class TokenStringLiteral extends Token
$stream = substr($stream, $i + 1); $stream = substr($stream, $i + 1);
return new TokenStringLiteral($buffer); return new StringLiteral($buffer);
}
/**
* Get the value of this token.
*
* @param ?\App\Util\GraphNode[] $arguments
* @return \App\Util\GraphNode|Token|scalar|null
*/
public function execute(?array $arguments = null)
{
return $this->getValue();
} }
} }

43
app/Engine/Cfnpp/Expression/TokenVariable.php → app/Cfnpp/Expression/Token/Variable.php

@ -2,23 +2,26 @@
declare(strict_types=1); declare(strict_types=1);
namespace App\Engine\Cfnpp\Expression; namespace App\Cfnpp\Expression\Token;
use App\Cfnpp\Expression\Token;
use App\Cfnpp\Expression\TokenLiteral;
/** /**
* Expression token referencing a variable. * Reference to a variable.
* *
* @author Adam Pippin <hello@adampippin.ca> * @author Adam Pippin <hello@adampippin.ca>
*/ */
class TokenVariable extends Token class Variable extends TokenLiteral
{ {
/** /**
* Name of the variable this token references. * Name of the variable.
* @var string * @var string
*/ */
protected $name; protected $name;
/** /**
* Create a new variable token. * New variable reference.
* *
* @param string $name * @param string $name
*/ */
@ -28,7 +31,7 @@ class TokenVariable extends Token
} }
/** /**
* Get the name of the variable this token references. * Get the name of the variable.
* *
* @return string * @return string
*/ */
@ -37,25 +40,11 @@ class TokenVariable extends Token
return $this->name; return $this->name;
} }
/**
* Determine whether a variable name token can be parsed from a stream.
*
* @param string $stream
* @return bool
*/
public static function isToken(string $stream): bool public static function isToken(string $stream): bool
{ {
return (bool)preg_match('/^[A-Za-z]$/', $stream[0]); return (bool)preg_match('/^[A-Za-z]$/', $stream[0]);
} }
/**
* Parse a variable token from a stream.
*
* Returns token, and modifies stream to remove all consumed characters.
*
* @param string $stream
* @return Token
*/
public static function getToken(string &$stream): Token public static function getToken(string &$stream): Token
{ {
$buffer = ''; $buffer = '';
@ -74,6 +63,18 @@ class TokenVariable extends Token
$stream = substr($stream, strlen($buffer)); $stream = substr($stream, strlen($buffer));
return new TokenVariable($buffer); return new Variable($buffer);
}
/**
* Not valid. Variable replacement should be handled by the expression parser.
*
* @param ?\App\Util\GraphNode[] $arguments
* @throws \Exception if called
* @return \App\Util\GraphNode|Token|scalar|null
*/
public function execute(?array $arguments = null)
{
throw new \Exception('Unreplaced variable');
} }
} }

12
app/Cfnpp/Expression/TokenBinary.php

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace App\Cfnpp\Expression;
/**
* A token that takes two arguments.
*/
abstract class TokenBinary extends Token
{
}

12
app/Cfnpp/Expression/TokenLiteral.php

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace App\Cfnpp\Expression;
/**
* A token that takes no arguments.
*/
abstract class TokenLiteral extends Token
{
}

12
app/Cfnpp/Expression/TokenUnary.php

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace App\Cfnpp\Expression;
/**
* A token that takes one argument.
*/
abstract class TokenUnary extends Token
{
}

105
app/Engine/Cfnpp/Functions.php → app/Cfnpp/Functions.php

@ -2,7 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
namespace App\Engine\Cfnpp; namespace App\Cfnpp;
use App\Dom\Node; use App\Dom\Node;
use App\Dom\NodeValue; use App\Dom\NodeValue;
@ -16,6 +16,13 @@ use App\Dom\NodeFunctionValue;
*/ */
class Functions class Functions
{ {
/**
* For now, generate incremental condition names.
* @todo
* @var int
*/
public static $condition_idx = 0;
/** /**
* cfnpp compiler. * cfnpp compiler.
* @var Compiler * @var Compiler
@ -76,7 +83,10 @@ class Functions
*/ */
public function mf_replace(Node $original, Node $target, NodeFunction $function): ?Node public function mf_replace(Node $original, Node $target, NodeFunction $function): ?Node
{ {
// TODO: Deal with nodefunctionvalue if ($function instanceof NodeFunctionValue)
{
return new NodeValue(null, $target->hasName() ? $target->getName() : null, $function->getValue());
}
$replacement = new Node(null, $target->hasName() ? $target->getName() : null); $replacement = new Node(null, $target->hasName() ? $target->getName() : null);
$replacement->setChildren($function->getChildren()); $replacement->setChildren($function->getChildren());
@ -112,6 +122,25 @@ class Functions
return new NodeValue(null, $node->hasName() ? $node->getName() : null, $value); return new NodeValue(null, $node->hasName() ? $node->getName() : null, $value);
} }
/**
* Create a reference to a CloudFormation parameter.
*
* @param Node $node
* @param NodeFunction $function
* @return ?Node
*/
public function f_param(Node $node, NodeFunction $function): ?Node
{
if (!($function instanceof NodeFunctionValue))
{
throw new \Exception('!param requires scalar argument');
}
$node = new Node(null, $node->hasName() ? $node->getName() : null);
$ref = new NodeValue($node, 'Ref', $function->getValue());
$node->addChild($ref);
return $node;
}
/** /**
* Conditionally include part of the file. * Conditionally include part of the file.
* *
@ -137,26 +166,47 @@ class Functions
$if_true = $nodes[1]; $if_true = $nodes[1];
$if_false = sizeof($nodes) == 3 ? $nodes[2] : null; $if_false = sizeof($nodes) == 3 ? $nodes[2] : null;
$parser = new \App\Engine\Cfnpp\Expression\Parser(); $expression = new \App\Cfnpp\Expression\Expression($condition->getValue());
$expression = $parser->parse($condition->getValue()); $expression->solve($this->options);
$result = $expression->evaluate($this->options->getVariables());
if (is_array($result)) // We need a single resulting node as a final value either as a scalar or
// if we want to convert to cloudformation.
if ($expression->count() > 1)
{ {
throw new \Exception('!if expression must evaluate down to a single value: '.$condition->getValue()); throw new \Exception('!if expression must evaluate down to a single value: '.$condition->getValue());
} }
if ($result) if ($expression->isComplete())
{ {
$if_true->setName($node->hasName() ? $node->getName() : null); // We know the final result, we can resolve this directly.
return $if_true; if ($expression->getValue())
{
$if_true->setName($node->hasName() ? $node->getName() : null);
return $if_true;
}
elseif (isset($if_false))
{
$if_false->setName($node->hasName() ? $node->getName() : null);
return $if_false;
}
return null;
} }
// Create condition
// Generate Cfn Fn::If
$condition_name = 'Condition'.(static::$condition_idx++);
$this->compiler->addCondition($condition_name, Node::fromPhp($expression->toCloudformation()));
$n_orig = new Node(null, $node->hasName() ? $node->getName() : null);
$n_if = new Node($n_orig, 'Fn::If');
$n_orig->addChild($n_if);
$n_if->addChild(new NodeValue($n_if, null, $condition_name));
$n_if->addChild($if_true);
if (isset($if_false)) if (isset($if_false))
{ {
$if_false->setName($node->hasName() ? $node->getName() : null); $n_if->addChild($if_false);
} }
return $if_false; return $n_orig;
} }
/** /**
@ -170,16 +220,35 @@ class Functions
{ {
if (!($function instanceof NodeFunctionValue)) if (!($function instanceof NodeFunctionValue))
{ {
throw new \Exception('!if requires scalar argument'); throw new \Exception('!expr requires scalar argument');
} }
$parser = new \App\Engine\Cfnpp\Expression\Parser(); $expression = new \App\Cfnpp\Expression\Expression($function->getValue());
$expression = $parser->parse($function->getValue()); $expression->solve($this->options);
$result = $expression->evaluate($this->options->getVariables());
$result = Node::fromPhp($result); if ($expression->isComplete())
$result->setName($node->hasName() ? $node->getName() : null); {
// If we computed a final value/set of values, we can just insert those
// directly.
if ($expression->count() == 1)
{
$solution_node = Node::fromPhp($expression->getValue());
}
else
{
$solution_node = Node::fromPhp($expression->toArray());
}
}
else
{
// Otherwise let's convert it to cfn intrinsics
$solution_node = Node::fromPhp($expression->toCloudformation());
}
return $result; if ($node->hasName())
{
$solution_node->setName($node->getName());
}
return $solution_node;
} }
} }

2
app/Engine/Cfnpp/Options.php → app/Cfnpp/Options.php

@ -2,7 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
namespace App\Engine\Cfnpp; namespace App\Cfnpp;
/** /**
* Options for controlling the Cfnpp compilation process. * Options for controlling the Cfnpp compilation process.

4
app/Commands/Stack/Compile.php

@ -39,8 +39,8 @@ class Compile extends Command
$serializer = $this->getSerializer(); $serializer = $this->getSerializer();
$unserializer = $this->getUnserializer(); $unserializer = $this->getUnserializer();
$engine->setSerializer($serializer)->setUnserializer($unserializer)->setCompiler(new \App\Engine\Cfnpp\Compiler()); $engine->setSerializer($serializer)->setUnserializer($unserializer)->setCompiler(new \App\Cfnpp\Compiler());
$options = new \App\Engine\Cfnpp\Options(); $options = new \App\Cfnpp\Options();
$output = $engine->process($this->argument('in_file'), $options); $output = $engine->process($this->argument('in_file'), $options);

231
app/Engine/Cfnpp/Expression/Expression.php

@ -1,231 +0,0 @@
<?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;
}
/**
* 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<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));
$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);
}
}

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

@ -1,61 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Engine\Cfnpp\Expression;
/**
* Parse a string into a series of expression tokens.
*
* @author Adam Pippin <hello@adampippin.ca>
*/
class Parser
{
/**
* List of class names of tokens this parser will parse.
*
* @var string[]
*/
public const TOKEN_TYPES = [
TokenFunction::class,
TokenNumericLiteral::class,
TokenOperator::class,
TokenStringLiteral::class,
TokenVariable::class
];
/**
* Parse a string into an expression.
*
* @param string $value
* @return Expression
*/
public function parse(string $value): Expression
{
$original_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);
}
}

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

@ -1,32 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Engine\Cfnpp\Expression;
/**
* Token representing a distinct part of an expression.
*
* @author Adam Pippin <hello@adampippin.ca>
*/
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;
}

90
app/Engine/Cfnpp/Expression/TokenFunction.php

@ -1,90 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Engine\Cfnpp\Expression;
/**
* Token representing a function.
*
* @author Adam Pippin <hello@adampippin.ca>
*/
class TokenFunction extends Token
{
/**
* List of valid functions this can parse.
* @var string[]
*/
public const FUNCTIONS = [
'concat*',
'concat'
];
/**
* Function this token represents.
* @var string
*/
protected $function;
/**
* Create a new function token.
*
* @param string $function
*/
public function __construct($function)
{
$this->function = $function;
}
/**
* Get the function this token represents.
*
* @return string
*/
public function getFunction(): string
{
return $this->function;
}
/**
* Determine whether a function token can be parsed from a stream.
*
* @param string $stream
* @return bool
*/
public static function isToken(string $stream): bool
{
foreach (static::FUNCTIONS as $function)
{
if (strlen($stream) > strlen($function) &&
substr($stream, 0, strlen($function)) == $function)
{
return true;
}
}
return false;
}
/**
* Parse a function token from a stream.
*
* Returns token, and modifies stream to remove all consumed characters
*
* @param string $stream
* @return Token
*/
public static function getToken(string &$stream): Token
{
foreach (static::FUNCTIONS as $function)
{
if (strlen($stream) > strlen($function) &&
substr($stream, 0, strlen($function)) == $function)
{
$function = substr($stream, 0, strlen($function));
$stream = substr($stream, strlen($function));
return new TokenFunction($function);
}
}
throw new \Exception('Could not parse function token!');
}
}

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

@ -1,96 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Engine\Cfnpp\Expression;
/**
* Token representing a comparison operator.
*
* @author Adam Pippin <hello@adampippin.ca>
*/
class TokenOperator extends Token
{
/**
* List of valid operators this can parse.
* @var string[]
*/
public const OPERATORS = [
'eq',
'neq',
'gt',
'gte',
'lt',
'lte',
'and',
'or'
];
/**
* Operator this token represents.
* @var string
*/
protected $operator;
/**
* Create a new operator token.
*
* @param string $operator
*/
public function __construct($operator)
{
$this->operator = $operator;
}
/**
* Get the operator this token represents.
*
* @return string
*/
public function getOperator(): string
{
return $this->operator;
}
/**
* Determine whether a operator token can be parsed from a stream.
*
* @param string $stream
* @return bool
*/
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;
}
/**
* Parse an operator token from a stream.
*
* Returns token, and modifies stream to remove all consumed characters
*
* @param string $stream
* @return Token
*/
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);
}
}
throw new \Exception('Could not parse operator token!');
}
}

212
app/Util/GraphNode.php

@ -0,0 +1,212 @@
<?php
declare(strict_types=1);
namespace App\Util;
/**
* Represents a node in a tree.
*
* @author Adam Pippin <hello@adampippin.ca>
*/
class GraphNode
{
/**
* Parent of this node.
* @var GraphNode
*/
protected $parent;
/**
* Value of this node.
* @var mixed
*/
protected $value;
/**
* Children of this node.
* @var GraphNode[]
*/
protected $children;
/**
* Create a new graph node.
*
* @param mixed $value
*/
public function __construct($value = null)
{
$this->value = $value;
$this->children = [];
}
/**
* Set the value of this node.
*
* @param mixed $value
* @return void
*/
public function setValue($value): void
{
$this->value = $value;
}
/**
* Get the value of this node.
*
* @return mixed
*/
public function getValue()
{
return $this->value;
}
/**
* Set the parent node of this node.
*
* @param GraphNode $node
* @return void
*/
public function setParent(GraphNode $node): void
{
$this->parent = $node;
}
/**
* Determine whether this node has a parent.
*
* @return bool
*/
public function hasParent(): bool
{
return isset($this->parent);
}
/**
* Get this node's parent node.
*
* @return GraphNode
*/
public function getParent(): GraphNode
{
return $this->parent;
}
/**
* Count how many child nodes this node has.
*
* @return int
*/
public function countChildren(): int
{
return sizeof($this->children);
}
/**
* Determine whether this node has any children.
*
* @return bool
*/
public function hasChildren(): bool
{
return sizeof($this->children) > 0;
}
/**
* Fetch all of this node's children.
*
* @return GraphNode[]
*/
public function getChildren(): array
{
return $this->children;
}
/**
* Add a node to this child's list of children.
*
* @param GraphNode $child
* @return void
*/
public function appendChild(GraphNode $child): void
{
$this->children[] = $child;
$child->setParent($this);
}
/**
* Replace a child node with another node.
*
* @param GraphNode $original
* @param GraphNode $new
* @return void
*/
public function replaceChild(GraphNode $original, GraphNode $new): void
{
for ($i = 0; $i < sizeof($this->children); $i++)
{
if ($this->children[$i] === $original)
{
$this->children[$i] = $new;
$new->setParent($this);
return;
}
}
}
/**
* Remove all children of this node.
*
* @return void
*/
public function clearChildren(): void
{
$this->children = [];
}
/**
* Add a child node by value.
*
* @param mixed $value value to add as a child
* @return GraphNode the node created as a child
*/
public function add($value): GraphNode
{
$this->children[] = new GraphNode($value);
end($this->children)->setParent($this);
return end($this->children);
}
/**
* Walk through this node and all children, calling callback on each node.
*
* Callback is called on a node's children before a node
*
* @param callable $callback callback(GraphNode $node): void
* @return void
*/
public function walk(callable $callback)
{
static::walkNodes([$this], $callback);
}
/**
* Internal function for recursively visiting all nodes.
*
* @param GraphNode[] $nodes
* @param callable $callback
* @return void
*/
protected static function walkNodes(array $nodes, callable $callback)
{
foreach ($nodes as $node)
{
if ($node->hasChildren())
{
static::walkNodes($node->getChildren(), $callback);
}
$callback($node);
}
}
}

73
app/Util/Stack.php

@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace App\Util;
/**
* Basic stack data structure.
*
* @author Adam Pippin <hello@adampippin.ca>
*/
class Stack
{
/**
* Stack data.
* @var mixed[]
*/
protected $stack;
/**
* Create a new stack.
*/
public function __construct()
{
$this->stack = [];
}
/**
* Count how many items this stack contains.
*
* @return int
*/
public function count(): int
{
return sizeof($this->stack);
}
/**
* Get all items in this stack.
*
* @return mixed[]
*/
public function get(): array
{
return $this->stack;
}
/**
* Pop an item off of this stack.
*
* @param int $offset offset from the end to pop if not the last element
* @return mixed
*/
public function pop(int $offset = 0)
{
if (sizeof($this->stack) < $offset + 1)
{
throw new \Exception('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
*/
public function push($value): void
{
array_push($this->stack, $value);
}
}
Loading…
Cancel
Save