Compare commits

...

7 Commits

Author SHA1 Message Date
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. 113
      app/Cfnpp/Compiler.php
  2. 342
      app/Cfnpp/Expression/Expression.php
  3. 22
      app/Cfnpp/Expression/ICloudformationNative.php
  4. 40
      app/Cfnpp/Expression/Token.php
  5. 42
      app/Cfnpp/Expression/Token/NumericLiteral.php
  6. 181
      app/Cfnpp/Expression/Token/OperatorBinary.php
  7. 121
      app/Cfnpp/Expression/Token/OperatorUnary.php
  8. 91
      app/Cfnpp/Expression/Token/Parameter.php
  9. 40
      app/Cfnpp/Expression/Token/StringLiteral.php
  10. 43
      app/Cfnpp/Expression/Token/Variable.php
  11. 12
      app/Cfnpp/Expression/TokenBinary.php
  12. 12
      app/Cfnpp/Expression/TokenLiteral.php
  13. 12
      app/Cfnpp/Expression/TokenUnary.php
  14. 73
      app/Cfnpp/Functions.php
  15. 2
      app/Cfnpp/Options.php
  16. 4
      app/Commands/Stack/Compile.php
  17. 231
      app/Engine/Cfnpp/Expression/Expression.php
  18. 61
      app/Engine/Cfnpp/Expression/Parser.php
  19. 32
      app/Engine/Cfnpp/Expression/Token.php
  20. 90
      app/Engine/Cfnpp/Expression/TokenFunction.php
  21. 96
      app/Engine/Cfnpp/Expression/TokenOperator.php
  22. 212
      app/Util/GraphNode.php
  23. 73
      app/Util/Stack.php

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

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace App\Engine\Cfnpp;
namespace App\Cfnpp;
use App\Dom\Document;
use App\Dom\Node;
@ -41,6 +41,13 @@ class Compiler implements \App\Engine\ICompile
*/
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.
*
@ -66,6 +73,28 @@ class Compiler implements \App\Engine\ICompile
$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.
*
@ -81,7 +110,7 @@ class Compiler implements \App\Engine\ICompile
}
// Initialize state
$document = null;
$this->document = null;
$this->functions = [];
$this->merge_functions = [];
@ -89,12 +118,11 @@ class Compiler implements \App\Engine\ICompile
$cfnpp_functions = new Functions($this, $options);
$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_2($document, $options);
return $document;
return $this->document;
// Process each passed document
/*
foreach ($documents as $next_document)
@ -171,8 +199,8 @@ class Compiler implements \App\Engine\ICompile
}
/**
* Compiler Pass 1 - Grab all variables, build dependency graph of
* dependencies between variable values, resolve variables values. Then
* Compiler Pass 1 - Grab all variables and parameters, build dependency
* graph of dependencies between values, resolve values. Then
* set them on $options to include them in program state.
*
* @param Document $document
@ -182,32 +210,61 @@ class Compiler implements \App\Engine\ICompile
protected function pass_1(Document $document, IOptions $options): void
{
$variables_node = $document->getChildByPath('cfnpp.variables');
// If there are no variables, we're done here.
if (!isset($variables_node))
$parameters_node = $document->getChildByPath('cfnpp.parameters');
// If there are no variables or parameters, we're done here.
if (!isset($variables_node) && !isset($parameters_node))
{
return;
}
$variables_raw = [];
$nodes = [];
$graph = new DependencyGraph();
foreach ($variables_node as $variable_node)
if (isset($parameters_node))
{
$variables_raw[$variable_node->getName()] = $variable_node;
$graph->add($variable_node->getName(), $this->pass_1_getVariableDependencies($variable_node));
foreach ($parameters_node as $parameter_node)
{
$nodes[$parameter_node->getName()] = $parameter_node;
$graph->add($parameter_node->getName(), $this->pass_1_getDependencies($parameter_node));
}
}
$variables_ordered = $graph->solve();
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));
}
}
foreach ($variables_ordered as $variable)
$nodes_ordered = $graph->solve();
foreach ($nodes_ordered as $node_name)
{
$this->runFunctions($variables_raw[$variable]);
$variable_node = $variables_node->getChildByName($variable);
// By all accounts this _should_ still exist, but this accounts for the
// case where, say, someone had used !unset on a variable for some reason.
if (isset($variable_node))
$this->runFunctions($nodes[$node_name]);
$type = $nodes[$node_name]->getParent()->getName();
if ($type == 'parameters')
{
$options->setVariable($variable, Node::toPhp($variable_node));
$parameter_node = $parameters_node->getChildByName($node_name);
if (isset($parameter_node))
{
$options->setParameter($parameter_node->getName(), Node::toPhp($parameter_node));
}
}
elseif ($type == 'variables')
{
$variable_node = $variables_node->getChildByName($node_name);
if (isset($variable_node))
{
$options->setVariable($variable_node->getName(), Node::toPhp($variable_node));
}
}
}
}
@ -220,7 +277,7 @@ class Compiler implements \App\Engine\ICompile
* @param Node $node
* @return string[]
*/
protected function pass_1_getVariableDependencies(Node $node): array
protected function pass_1_getDependencies(Node $node): array
{
$stack = [$node];
@ -240,13 +297,19 @@ class Compiler implements \App\Engine\ICompile
{
$variables[] = $node->getValue();
}
elseif ($node instanceof NodeFunctionValue &&
$node->getName() == 'param')
{
$variables[] = $node->getValue();
}
/*
// TODO: Reimplement
elseif ($node instanceof NodeFunctionValue &&
$node->getName() == 'expr')
{
$parser = new \App\Engine\Cfnpp\Expression\Parser();
$expression = $parser->parse($node->getValue());
$expression = new \App\Cfnpp\Expression\Expression($node->getValue());
$variables = array_merge($variables, $expression->getReferencedVariables());
}
}*/
}
return $variables;

342
app/Cfnpp/Expression/Expression.php

@ -0,0 +1,342 @@
<?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\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
* @param \App\Engine\IOptions $options
*/
public function __construct(string $expression, \App\Engine\IOptions $options)
{
$tokens = static::tokenize($expression);
$nodes = static::parse($tokens);
$this->nodes = static::solve($nodes, $options->getVariables(), array_keys($options->getParameters()));
$this->options = $options;
}
/**
* 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);
}
/**
* 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);
}

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

@ -2,23 +2,26 @@
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>
*/
class TokenNumericLiteral extends Token
class NumericLiteral extends TokenLiteral
{
/**
* Numeric value this token represents.
* Value of this literal.
* @var int|float
*/
protected $value;
/**
* Create a new numeric literal.
* New number literal.
*
* @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
*/
@ -37,25 +40,11 @@ class TokenNumericLiteral extends Token
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
{
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
{
$buffer = '';
@ -82,6 +71,17 @@ class TokenNumericLiteral extends Token
$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();
}
}

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

@ -0,0 +1,181 @@
<?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'
];
/**
* 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 ? $value2 : false;
}
elseif (is_scalar($value2))
{
return $value2 ? $value1 : 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 : $value1;
}
elseif (is_scalar($value2))
{
return $value2 ? true : $value1;
}
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;
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
{
$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]];
default:
throw new \Exception('Missing implementation for unary operator: '.$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);
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>
*/
class TokenStringLiteral extends Token
class StringLiteral extends TokenLiteral
{
/**
* Value of the string literal.
@ -18,7 +21,7 @@ class TokenStringLiteral extends Token
protected $value;
/**
* Create a new string literal token.
* New string literal.
*
* @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
*/
@ -37,25 +40,11 @@ class TokenStringLiteral extends Token
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
{
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
{
$buffer = '';
@ -90,6 +79,17 @@ class TokenStringLiteral extends Token
$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);
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>
*/
class TokenVariable extends Token
class Variable extends TokenLiteral
{
/**
* Name of the variable this token references.
* Name of the variable.
* @var string
*/
protected $name;
/**
* Create a new variable token.
* New variable reference.
*
* @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
*/
@ -37,25 +40,11 @@ class TokenVariable extends Token
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
{
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
{
$buffer = '';
@ -74,6 +63,18 @@ class TokenVariable extends Token
$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
{
}

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

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace App\Engine\Cfnpp;
namespace App\Cfnpp;
use App\Dom\Node;
use App\Dom\NodeValue;
@ -16,6 +16,13 @@ use App\Dom\NodeFunctionValue;
*/
class Functions
{
/**
* For now, generate incremental condition names.
* @todo
* @var int
*/
public static $condition_idx = 0;
/**
* cfnpp compiler.
* @var Compiler
@ -112,6 +119,25 @@ class Functions
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.
*
@ -137,26 +163,42 @@ class Functions
$if_true = $nodes[1];
$if_false = sizeof($nodes) == 3 ? $nodes[2] : null;
$parser = new \App\Engine\Cfnpp\Expression\Parser();
$expression = $parser->parse($condition->getValue());
$result = $expression->evaluate($this->options->getVariables());
$expression = new \App\Cfnpp\Expression\Expression($condition->getValue(), $this->options);
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());
}
if ($result)
if ($expression->isComplete())
{
$if_true->setName($node->hasName() ? $node->getName() : null);
return $if_true;
// We know the final result, we can resolve this directly.
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;
}
if (isset($if_false))
{
$if_false->setName($node->hasName() ? $node->getName() : null);
}
return $if_false;
// 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_if->addChild(new NodeValue($n_if, null, $condition_name));
$n_if->addChild($if_true);
$n_if->addChild($if_false);
return $n_if;
}
/**
@ -166,6 +208,8 @@ class Functions
* @param NodeFunction $function
* @return ?Node
*/
/*
* TODO: Reimplement
public function f_expr(Node $node, NodeFunction $function): ?Node
{
if (!($function instanceof NodeFunctionValue))
@ -173,7 +217,7 @@ class Functions
throw new \Exception('!if requires scalar argument');
}
$parser = new \App\Engine\Cfnpp\Expression\Parser();
$parser = new \App\Cfnpp\Expression\Parser();
$expression = $parser->parse($function->getValue());
$result = $expression->evaluate($this->options->getVariables());
@ -182,4 +226,5 @@ class Functions
return $result;
}
*/
}

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

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

4
app/Commands/Stack/Compile.php

@ -39,8 +39,8 @@ class Compile extends Command
$serializer = $this->getSerializer();
$unserializer = $this->getUnserializer();
$engine->setSerializer($serializer)->setUnserializer($unserializer)->setCompiler(new \App\Engine\Cfnpp\Compiler());
$options = new \App\Engine\Cfnpp\Options();
$engine->setSerializer($serializer)->setUnserializer($unserializer)->setCompiler(new \App\Cfnpp\Compiler());
$options = new \App\Cfnpp\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