tokens = $tokens; $this->position = 0; $this->errors = []; $this->nodes = []; } public function parse(): array { $lastPosition = -1; while ($this->position < count($this->tokens) - 1) { $lastPosition = $this->position; $currentToken = $this->getCurrentToken(); $currentStatement = null; if ($currentToken->literal == "const") { $currentStatement = $this->parseConst(); } else if ($currentToken->literal == "if") { $currentStatement = $this->parseIf(); } else if ($currentToken->type == TokenType::Comment) { $currentStatement = new CommentNode($currentToken); $this->advance(1); } else if ($currentToken->type == TokenType::Identifier) { if ($this->getNextToken()->literal == "=") { $currentStatement = $this->parseVariableAssignment(); } else { $currentStatement = $this->parseFunctionCall(); } } else { $error = sprintf("Unexpected %s at %d:%d" . PHP_EOL, $currentToken->value, $currentToken->line, $currentToken->column); $this->addError($error); exit; } $nextToken = $this->getCurrentToken(); if ($nextToken->literal == "=>") { $this->advance(1); $currentStatement = new PipeExpression($currentStatement, $this->parseExpression(shouldBeFunction: true)); } // unknown token if ($this->position == $lastPosition) { $error = sprintf("Unknown token %s at position %d,%d" . PHP_EOL, $currentToken, $currentToken->line, $currentToken->column); $this->addError($error); $this->advance(1); } $this->nodes[] = $currentStatement; } return $this->nodes; } public function printTree(): void { $tree = new Tree($this->nodes); $this->printNodeRecursive($tree, 0); } private function printNodeRecursive(Node $node, int $level = 0): void { $properties = get_object_vars($node); foreach ($properties as $propertyKey => $propertyValue) { if ($propertyValue instanceof Node) { echo sprintf(str_repeat("> ", $level) . "%s: %s" . PHP_EOL, $propertyKey, (new \ReflectionClass($propertyValue))->getShortName()); $this->printNodeRecursive($propertyValue, $level + 1); } else if (is_array($propertyValue)) { $length = count($propertyValue); echo sprintf(str_repeat("> ", $level) . "%s[%d]:" . PHP_EOL, $propertyKey, $length); if ($length == 0) { echo str_repeat("> ", $level + 1) . "_empty_" . PHP_EOL; } else { foreach ($propertyValue as $idx => $item) { echo sprintf(str_repeat("> ", $level + 1) . "[%d]%s" . PHP_EOL, $idx + 1, (new \ReflectionClass($item))->getShortName()); $this->printNodeRecursive($item, $level + 2); } } } else { if (! empty($propertyValue)) { if ($propertyValue instanceof Token) { echo sprintf(str_repeat("> ", $level) . "%s: %s - %d:%d" . PHP_EOL, $propertyKey, $propertyValue->literal, $propertyValue->line, $propertyValue->column); } else { echo sprintf(str_repeat("> ", $level) . "%s: %s" . PHP_EOL, $propertyKey, $propertyValue); } } else { echo sprintf(str_repeat("> ", $level) . "%s: %s" . PHP_EOL, $propertyKey, "_empty_"); } } } } private function getCurrentToken(): Token { return $this->tokens[$this->position]; } private function getNextToken(int $steps = 1): ?Token { return $this->tokens[$this->position + $steps] ?? null; } private function advance(int $steps): void { $lastToken = $this->getCurrentToken(); if ($this->position + $steps > count($this->tokens)) { if (! empty($this->errors)) { exit; } assert( false, "Parser Implementation Error: Can't advance. Position out of bounds." ); } $this->position += $steps; } private function stepBack(int $steps): void { $this->position -= $steps; } private function anticipateTokenAndSkip(array|string $tokenValues): void { if (is_string($tokenValues)) { $tokenValues = [$tokenValues]; } foreach ($tokenValues as $value) { $currentToken = $this->getCurrentToken(); if ($currentToken->value != $value) { $error = sprintf("Expected %s but got %s instead at %d:%d" . PHP_EOL, $value, $currentToken->value, $currentToken->line, $currentToken->column); $this->addError($error); } $this->advance(1); } } private function addError(string $error): void { $this->errors[] = $error; echo $error; } private function parseConst(): Node { // skip const $this->anticipateTokenAndSkip("const"); $identifier = new IdentifierNode($this->getCurrentToken()); $this->advance(1); // skip : $this->anticipateTokenAndSkip(":"); $type = $this->parseType(); $expression = null; if ($this->getCurrentToken()->literal == "=") { // skip = $this->anticipateTokenAndSkip("="); $expression = $this->parseExpression( shouldBeMap: $type instanceof MapTypeDeclaration, shouldBeFunction: $type instanceof TypeDeclaration and $type->left->literal == "function", ); } return new ConstVariableDeclaration( $identifier, $type, $expression, ); } private function parseVariableAssignment(): Node { $identifier = new IdentifierNode($this->getCurrentToken()); $this->advance(1); // skip = $this->anticipateTokenAndSkip("="); $expression = $this->parseExpression( #shouldBeMap: $type instanceof MapTypeDeclaration, #shouldBeFunction: $type instanceof TypeDeclaration and $type->left->literal == "function", ); return new VariableAssignment( $identifier, $expression, ); } private function parseType(): Node { $currentToken = $this->getCurrentToken(); $nextToken = $this->getNextToken(); if ($currentToken->literal == "[") { return $this->parseArrayOrMapType(); } else if (in_array($nextToken->literal, ["and", "or"])) { $this->advance(2); return new TypeDeclaration($currentToken, $nextToken, $this->parseType()); } else { $this->advance(1); return new TypeDeclaration($currentToken); } } private function parseArrayOrMapType(): Node { // skip first [ $this->anticipateTokenAndSkip("["); $key = $this->parseType(); $currentToken = $this->getCurrentToken(); $nextToken = $this->getNextToken(); if ($currentToken->literal == "]" && $nextToken->literal == "[") { // skip to first type $this->advance(2); $value = $this->parseType(); // skip last ] $this->anticipateTokenAndSkip("]"); return new MapTypeDeclaration($key, $value); } else { // skip last ] $this->anticipateTokenAndSkip("]"); return new ArrayTypeDeclaration($key); } } private function parseExpression($shouldBeMap = false, $shouldBeFunction = false): Node|Token { $currentToken = $this->getCurrentToken(); $currentExpression = $currentToken; if ($currentToken->literal == "[") { $currentExpression = $this->parseArrayOrMap($shouldBeMap); } else if ($currentToken->type == TokenType::Identifier) { if (in_array($currentToken->literal, ["true", "false"])) { $currentExpression = new BoolNode($currentToken); $this->advance(1); } else { $currentExpression = new IdentifierNode($currentToken); $this->advance(1); if ($this->getCurrentToken()->literal == "(") { // step back to parse function call fully $this->stepBack(1); $currentExpression = $this->parseFunctionCall(); } } } else if ($currentToken->type == TokenType::Number) { $currentExpression = $this->parseNumber(); } else if ($currentToken->type == TokenType::String) { $currentExpression = new StringNode($currentToken); $this->advance(1); } else if ($currentToken->type == TokenType::PipePlaceholder) { $currentExpression = new IdentifierNode($currentToken); $this->advance(1); } else if ($currentToken->literal == "(") { if ($this->getNextToken(2)->literal == ":") { $currentExpression = $this->parseFunctionDefinition(); } else { $currentExpression = $this->parseParenthesis(); } } else { # TODO: error. unknown token $this->advance(1); } $nextToken = $this->getCurrentToken(); if (in_array($nextToken->literal, ["+", "-", "*", "**", "/"])) { $this->advance(1); return new OperatorExpression($currentExpression, $nextToken, $this->parseExpression(shouldBeMap: $shouldBeMap)); } else if (in_array($nextToken->literal, ["<", ">", "<=", ">=", "==", "!="])) { $this->advance(1); return new CompareExpression($currentExpression, $nextToken, $this->parseExpression(shouldBeMap: $shouldBeMap)); } else if (in_array($nextToken->literal, ["and", "or"])) { $this->advance(1); return new Condition($currentExpression, $nextToken, $this->parseExpression(shouldBeMap: $shouldBeMap)); } else if ($nextToken->literal == "[") { return $this->parseArrayOrMapAccess($currentExpression); } else if ($nextToken->literal == "=>") { $this->advance(1); return new PipeExpression($currentExpression, $this->parseExpression(shouldBeFunction: true)); } else { return $currentExpression; } } private function parseArrayOrMap($shouldBeMap = false): Node { $values = []; // skip first [ if ($this->getCurrentToken()->literal == "[") { $this->advance(1); } $tokenShouldBeComma = false; while ($this->getCurrentToken()->literal != "]") { $currentToken = $this->getCurrentToken(); // skip , if ($tokenShouldBeComma) { if ($currentToken->literal == ",") { $this->advance(1); $tokenShouldBeComma = false; continue; } // , missing => error else { $error = sprintf( "Expected \",\" at position %d,%d - got %s instead" . PHP_EOL, $currentToken->line, $currentToken->column, $currentToken->literal ); $this->errors[] = $error; echo $error; break; } } // nested array or map if ($currentToken->literal == "[") { $values[] = $this->parseArrayOrMap(); $tokenShouldBeComma = true; } // skip comments else if ($currentToken->type == TokenType::Comment) { $this->advance(1); continue; } else { $values[] = $this->parseArrayOrMapItem(); $tokenShouldBeComma = true; } } // skip last ] $this->advance(1); if ((count($values) > 0 and $values[0] instanceof MapItemNode) or $shouldBeMap) { return new MapNode($values); } else { return new ArrayNode($values); } } private function parseArrayOrMapItem(): Node|Token { $key = $this->parseExpression(); // is map item if ($this->getCurrentToken()->literal == "=") { $this->advance(1); $value = $this->parseExpression(); return new MapItemNode($key, $value); } // is array item else { return $key; } } private function parseArrayOrMapAccess(Node $arrayOrMap): Node { $items = []; while ($this->getCurrentToken()->literal == "[") { // skip current [ $this->anticipateTokenAndSkip("["); $item = $this->parseExpression(); // skip last ] $this->anticipateTokenAndSkip("]"); $items[] = $item; } return new ArrayMapAccessNode( $arrayOrMap, $items, ); } private function parseNumber(): Node { $currentToken = $this->getCurrentToken(); $value = $currentToken->value; if (str_contains($value, ".")) { $value = floatval($value); } else { $value = intval($value); } // step to next token $this->advance(1); return new NumberNode($currentToken, $value); } private function parseFunctionDefinition(): Node { $parameters = $this->parseFunctionDefinitionParameters(); // skip : $this->anticipateTokenAndSkip(":"); $returnType = $this->parseType(); $body = $this->parseFunctionBody(); return new FunctionDefinition( $parameters, $returnType, $body, ); } private function parseFunctionDefinitionParameters(): array { // skip first ( if ($this->getCurrentToken()->literal == "(") { $this->advance(1); } $parameters = []; while ($this->getCurrentToken()->literal != ")") { // skip , if ($this->getCurrentToken()->literal == ",") { $this->advance(1); continue; } $identifier = new IdentifierNode($this->getCurrentToken()); $this->advance(1); // skip : $this->anticipateTokenAndSkip(":"); $type = $this->parseType(); // skip potential , instead default value if ($this->getCurrentToken()->literal == ",") { $this->advance(1); } // default value $defaultValue = null; if ($this->getCurrentToken()->literal == "=") { $this->advance(1); $defaultValue = $this->parseExpression(); } $parameters[] = new FunctionDefinitionParameter( $identifier, $type, $defaultValue, ); } // skip last ) $this->anticipateTokenAndSkip(")"); return $parameters; } private function parseFunctionBody(): array { // skip first { $this->anticipateTokenAndSkip("{"); $body = []; while ($this->getCurrentToken()->literal != "}") { $currentToken = $this->getCurrentToken(); if ($currentToken->literal == "const") { $body[] = $this->parseConst(); } else if ($currentToken->literal == "return") { $body[] = $this->parseFunctionReturn(); } else if ($currentToken->literal == "if") { $body[] = $this->parseIf(); } else if ($currentToken->type == TokenType::Comment) { $body[] = new CommentNode($currentToken); $this->advance(1); } else if ($currentToken->type == TokenType::Identifier) { if ($this->getNextToken()->literal == "=") { $body[] = $this->parseVariableAssignment(); } else { $body[] = $this->parseFunctionCall(); } } else { $error = sprintf("Unexpected %s at %d:%d" . PHP_EOL, $currentToken->value, $currentToken->line, $currentToken->column); $this->addError($error); exit; } } // skip last } $this->anticipateTokenAndSkip("}"); return $body; } private function parseFunctionReturn(): Node { // skip return $this->anticipateTokenAndSkip("return"); return new FunctionReturn($this->parseExpression()); } private function parseFunctionCall(): Node { $identifier = new IdentifierNode($this->getCurrentToken()); $this->advance(1); // skip first ( $this->anticipateTokenAndSkip("("); $parameters = $this->parseFunctionCallParameters(); return new FunctionCall( $identifier, $parameters, ); } private function parseFunctionCallParameters(): array { // skip first ( if ($this->getCurrentToken()->literal == "(") { $this->advance(1); } $parameters = []; while ($this->getCurrentToken()->literal != ")") { // skip , if ($this->getCurrentToken()->literal == ",") { $this->advance(1); continue; } // if "=" then identifier is name if ($this->getNextToken()->literal == "=") { $identifier = new IdentifierNode($this->getCurrentToken()); $this->advance(2); $value = $this->parseExpression(); $parameters[] = new FunctionCallParameter( $identifier, $value, ); } // else only value is provided else { $parameters[] = new FunctionCallParameter( null, $this->parseExpression(), ); } } // skip last ) $this->anticipateTokenAndSkip(")"); return $parameters; } private function parseIf(): Node { $arms = []; // parse first arm $arms[] = $this->parseIfArm(); // parse remaining arms while ($this->getCurrentToken()->literal == "else") { $arms[] = $this->parseIfArm(); } return new IfNode($arms); } private function parseIfArm(): Node { $isElseBlock = false; if ($this->getCurrentToken()->literal == "if") { $this->advance(1); } else if ($this->getCurrentToken()->literal == "else") { if ($this->getNextToken()->literal == "if") { $this->advance(2); } else { $isElseBlock = true; $this->advance(1); } } $condition = null; if (! $isElseBlock) { // skip first ( $this->anticipateTokenAndSkip("("); $condition = $this->parseExpression(); // skip last ) $this->anticipateTokenAndSkip(")"); } $body = $this->parseFunctionBody(); return new IfArm( $condition, $body, ); } private function parseParenthesis(): Node { // skip first ( $this->anticipateTokenAndSkip("("); $content = $this->parseExpression(); // skip last ) $this->anticipateTokenAndSkip(")"); return new Parenthesis($content); } } class Node {} class Tree extends Node { /** * @param Node[] $nodes */ public function __construct( public array $nodes, ) {} } class ConstVariableDeclaration extends Node { public function __construct( public Node $identifier, public Node|Token $type, public Node|Token|null $expression, ) {} } class VariableAssignment extends Node { public function __construct( public Node $identifier, public Node|Token $expression, ) {} } class TypeDeclaration extends Node { public function __construct( public Token $left, public ?Token $operator = null, public Token|TypeDeclaration|null $right = null, ) {} } class ArrayTypeDeclaration extends Node { public function __construct( public TypeDeclaration|ArrayTypeDeclaration $value, ) {}} class MapTypeDeclaration extends Node { public function __construct( public TypeDeclaration $key, public TypeDeclaration|ArrayTypeDeclaration $value, ) {} } class ArrayNode extends Node { /** * @param Array $values */ public function __construct( public array $values, ) {} public function getValue(): array { return $this->values; } } class MapNode extends Node { /** * @param MapItemNode[] $values */ public function __construct( public array $values, ) {} public function getValue(): array { return $this->values; } } class MapItemNode extends Node { public function __construct( public Token|Node $key, public Token|Node $value, ) {} } class ArrayMapAccessNode extends Node { public function __construct( public ArrayNode|IdentifierNode $arrayOrMap, /** * @param Node[] $items */ public array $items, ) {} } class OperatorExpression extends Node { public function __construct( public Token|Node $left, public Token $operator, public Token|Node $right, ) {} } class CompareExpression extends Node { public function __construct( public Token|Node $left, public Token $operator, public Token|Node $right, ) {} } class Condition extends Node { public function __construct( public Token|Node $left, public Token $operator, public Token|Node $right, ) {} } class IdentifierNode extends Node { public function __construct( public Token $token, ) {} } class NumberNode extends Node { public function __construct( public Token $token, public int|float $value, ) {} public function getValue(): int|float { return $this->value; } } class StringNode extends Node { public function __construct( public Token $token, ) {} public function getValue(): string { return $this->token->value; } } class BoolNode extends Node { public function __construct( public Token $token, ) {} public function getValue(): bool { return filter_var($this->token->value, FILTER_VALIDATE_BOOLEAN); } } class CommentNode extends Node { public function __construct( public Token $token, ) {} } class FunctionDefinition extends Node { public function __construct( /** * @param FunctionDefinitionParameter[] $parameters */ public array $parameters, public TypeDeclaration|ArrayTypeDeclaration|MapTypeDeclaration $returnType, /** * @param Node[] $body */ public array $body, ) {} } class FunctionDefinitionParameter extends Node { public function __construct( public Node $identifier, public TypeDeclaration|ArrayTypeDeclaration|MapTypeDeclaration $type, public ?Node $defaultValue = null, ) {} } class FunctionReturn extends Node { public function __construct( public Node|Token $returnValue, ) {} } class FunctionCall extends Node { public function __construct( public Node $identifier, /** * @param FunctionCallParameter[] $parameters */ public array $parameters, ) {} } class FunctionCallParameter extends Node { public function __construct( public ?Node $identifier, public Node|Token $value, ) {} } class IfNode extends Node { public function __construct( /** * @param IfArm[] $arms */ public array $arms, ) {} } class IfArm extends Node { public function __construct( public Condition|CompareExpression|null $condition, public array $body, ) {} } class PipeExpression extends Node { public function __construct( public Token|Node $left, public Token|Node $right, ) {} } class Parenthesis extends Node { public function __construct( public Node $content, ) {} }