From a6bc964911192719743ffa7bf8a30e9131180779 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Thu, 18 May 2023 21:41:59 +0200 Subject: [PATCH 01/20] WIP: Feature: Try out parsica --- composer.json | 3 +- composer.lock | 136 +++++++++++++++++- src/Definition/Precedence.php | 38 ++++- src/Parser/Ast/AccessChainSegmentNode.php | 2 +- src/Parser/Ast/AccessChainSegmentNodes.php | 2 +- src/Parser/Ast/AccessNode.php | 2 +- src/Parser/Ast/BinaryOperationNode.php | 2 +- src/Parser/Ast/BooleanLiteralNode.php | 2 +- src/Parser/Ast/ExpressionNode.php | 13 +- src/Parser/Ast/IdentifierNode.php | 9 +- src/Parser/Ast/NullLiteralNode.php | 2 +- src/Parser/Ast/NumberLiteralNode.php | 2 +- src/Parser/Ast/StringLiteralNode.php | 9 +- src/Parser/Ast/TernaryOperationNode.php | 2 +- src/Parser/Ast/UnaryOperationNode.php | 2 +- src/Parser/Parser/Access/AccessParser.php | 61 ++++++++ .../BinaryOperation/BinaryOperationParser.php | 79 ++++++++++ .../BooleanLiteral/BooleanLiteralParser.php | 46 ++++++ .../Parser/Expression/ExpressionParser.php | 84 +++++++++++ .../Parser/Identifier/IdentifierParser.php | 44 ++++++ .../Parser/NullLiteral/NullLiteralParser.php | 43 ++++++ .../NumberLiteral/NumberLiteralParser.php | 49 +++++++ .../StringLiteral/StringLiteralParser.php | 67 +++++++++ .../TernaryOperationParser.php | 43 ++++++ .../UnaryOperation/UnaryOperationParser.php | 55 +++++++ 25 files changed, 761 insertions(+), 36 deletions(-) create mode 100644 src/Parser/Parser/Access/AccessParser.php create mode 100644 src/Parser/Parser/BinaryOperation/BinaryOperationParser.php create mode 100644 src/Parser/Parser/BooleanLiteral/BooleanLiteralParser.php create mode 100644 src/Parser/Parser/Expression/ExpressionParser.php create mode 100644 src/Parser/Parser/Identifier/IdentifierParser.php create mode 100644 src/Parser/Parser/NullLiteral/NullLiteralParser.php create mode 100644 src/Parser/Parser/NumberLiteral/NumberLiteralParser.php create mode 100644 src/Parser/Parser/StringLiteral/StringLiteralParser.php create mode 100644 src/Parser/Parser/TernaryOperation/TernaryOperationParser.php create mode 100644 src/Parser/Parser/UnaryOperation/UnaryOperationParser.php diff --git a/composer.json b/composer.json index 3945c9ea..791bc877 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,8 @@ } ], "require": { - "php": ">=8.1" + "php": ">=8.1", + "parsica-php/parsica": "^0.8.1" }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock index 1870f7ce..350994e4 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,140 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "6c5c2298ea51fcc949f34ec1d7d401ea", - "packages": [], + "content-hash": "a99384d56f855ec9ad09086c941c4249", + "packages": [ + { + "name": "cypresslab/php-curry", + "version": "0.5.0", + "source": { + "type": "git", + "url": "https://github.com/matteosister/php-curry.git", + "reference": "cf1a856b4bfcf6ba671296f1d1216323f366177e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/matteosister/php-curry/zipball/cf1a856b4bfcf6ba671296f1d1216323f366177e", + "reference": "cf1a856b4bfcf6ba671296f1d1216323f366177e", + "shasum": "" + }, + "require": { + "php": ">=5.3.2" + }, + "require-dev": { + "phpunit/phpunit": "~6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "files": [ + "src/Cypress/Curry/functions.php" + ], + "psr-0": { + "Cypress\\Curry": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Curried functions in PHP", + "keywords": [ + "curry", + "functional-programming" + ], + "support": { + "issues": "https://github.com/matteosister/php-curry/issues", + "source": "https://github.com/matteosister/php-curry/tree/0.5.0" + }, + "time": "2017-03-10T17:05:37+00:00" + }, + { + "name": "parsica-php/parsica", + "version": "0.8.1", + "source": { + "type": "git", + "url": "https://github.com/parsica-php/parsica.git", + "reference": "1f1ec12f0bd1fe09bab1933fe6d9566d8b6c40b8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/parsica-php/parsica/zipball/1f1ec12f0bd1fe09bab1933fe6d9566d8b6c40b8", + "reference": "1f1ec12f0bd1fe09bab1933fe6d9566d8b6c40b8", + "shasum": "" + }, + "require": { + "cypresslab/php-curry": "^0.5.0", + "ext-mbstring": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "ext-json": "*", + "mathiasverraes/uptodocs": "dev-main", + "phpbench/phpbench": "dev-master", + "phpunit/phpunit": "^9.0", + "psr/event-dispatcher": "^1.0", + "vimeo/psalm": "^4.1" + }, + "type": "library", + "autoload": { + "files": [ + "src/characters.php", + "src/combinators.php", + "src/numeric.php", + "src/predicates.php", + "src/primitives.php", + "src/recursion.php", + "src/sideEffects.php", + "src/space.php", + "src/strings.php", + "src/Expression/expression.php", + "src/Internal/FP.php" + ], + "psr-4": { + "Parsica\\Parsica\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mathias Verraes", + "email": "mathias@verraes.net", + "homepage": "https://verraes.net" + }, + { + "name": "Toon Daelman", + "email": "spinnewebber_toon@hotmail.com", + "homepage": "https://github.com/turanct" + } + ], + "description": "The easiest way to build robust parsers in PHP.", + "homepage": "https://parsica-php.github.io/", + "keywords": [ + "parser", + "parser combinator", + "parser-combinator", + "parsing" + ], + "support": { + "issues": "https://github.com/parsica-php/parsica/issues", + "source": "https://github.com/parsica-php/parsica/tree/0.8.1" + }, + "funding": [ + { + "url": "https://github.com/turanct", + "type": "github" + } + ], + "time": "2021-04-10T12:58:34+00:00" + } + ], "packages-dev": [ { "name": "doctrine/instantiator", diff --git a/src/Definition/Precedence.php b/src/Definition/Precedence.php index 49da1b56..7ab95494 100644 --- a/src/Definition/Precedence.php +++ b/src/Definition/Precedence.php @@ -23,7 +23,7 @@ namespace PackageFactory\ComponentEngine\Definition; use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; - +use Parsica\Parsica\Stream; enum Precedence: int @@ -43,6 +43,19 @@ enum Precedence: int case TERNARY = 3; case SEQUENCE = 1; + private const ARRAY_OF_STRINGS_TO_PRECEDENCE = [ + [['(', ')', '[', ']', '?.', '.'], self::ACCESS], + [['!'], self::UNARY], + [['*', '/', '%'], self::POINT], + [['+', '-'], self::DASH], + [['+', '-'], self::DASH], + [['>', '>=', '<', '<='], self::COMPARISON], + [['===', '!=='], self::EQUALITY], + [['&&'], self::LOGICAL_AND], + [['||'], self::LOGICAL_OR], + [['?', ':'], self::TERNARY] + ]; + public static function forTokenType(TokenType $tokenType): self { return match ($tokenType) { @@ -81,8 +94,27 @@ public static function forTokenType(TokenType $tokenType): self }; } - public function mustStopAt(TokenType $tokenType): bool + public static function fromRemainder(Stream $stream): self + { + // @todo dont rely on `__toString` as its an implementation detail and the stream could be lazy + $contents = $stream->__toString(); + foreach (self::ARRAY_OF_STRINGS_TO_PRECEDENCE as [$strings, $pre]) { + foreach ($strings as $string) { + if (str_starts_with($contents, $string)) { + return $pre; + } + } + } + return self::SEQUENCE; + } + + public function mustStopAt(self $precedence): bool + { + return $precedence->value <= $this->value; + } + + public function mustStopAtTokenType(TokenType $tokenType): bool { - return self::forTokenType($tokenType)->value <= $this->value; + return $this->mustStopAt(self::forTokenType($tokenType)); } } diff --git a/src/Parser/Ast/AccessChainSegmentNode.php b/src/Parser/Ast/AccessChainSegmentNode.php index bad435b6..743cf4cb 100644 --- a/src/Parser/Ast/AccessChainSegmentNode.php +++ b/src/Parser/Ast/AccessChainSegmentNode.php @@ -29,7 +29,7 @@ final class AccessChainSegmentNode implements \JsonSerializable { - private function __construct( + public function __construct( public readonly AccessType $accessType, public readonly IdentifierNode $accessor ) { diff --git a/src/Parser/Ast/AccessChainSegmentNodes.php b/src/Parser/Ast/AccessChainSegmentNodes.php index 76f8e7de..0a0b2790 100644 --- a/src/Parser/Ast/AccessChainSegmentNodes.php +++ b/src/Parser/Ast/AccessChainSegmentNodes.php @@ -33,7 +33,7 @@ final class AccessChainSegmentNodes implements \JsonSerializable */ public readonly array $items; - private function __construct( + public function __construct( AccessChainSegmentNode ...$items ) { $this->items = $items; diff --git a/src/Parser/Ast/AccessNode.php b/src/Parser/Ast/AccessNode.php index dc75bcfd..311a7bca 100644 --- a/src/Parser/Ast/AccessNode.php +++ b/src/Parser/Ast/AccessNode.php @@ -26,7 +26,7 @@ final class AccessNode implements \JsonSerializable { - private function __construct( + public function __construct( public readonly ExpressionNode $root, public readonly AccessChainSegmentNodes $chain ) { diff --git a/src/Parser/Ast/BinaryOperationNode.php b/src/Parser/Ast/BinaryOperationNode.php index f3dcd3bd..66362e51 100644 --- a/src/Parser/Ast/BinaryOperationNode.php +++ b/src/Parser/Ast/BinaryOperationNode.php @@ -28,7 +28,7 @@ final class BinaryOperationNode implements \JsonSerializable { - private function __construct( + public function __construct( public readonly ExpressionNode $left, public readonly BinaryOperator $operator, public readonly ExpressionNode $right diff --git a/src/Parser/Ast/BooleanLiteralNode.php b/src/Parser/Ast/BooleanLiteralNode.php index 0af52a8d..21a0c31e 100644 --- a/src/Parser/Ast/BooleanLiteralNode.php +++ b/src/Parser/Ast/BooleanLiteralNode.php @@ -28,7 +28,7 @@ final class BooleanLiteralNode implements \JsonSerializable { - private function __construct( + public function __construct( public readonly bool $value ) { } diff --git a/src/Parser/Ast/ExpressionNode.php b/src/Parser/Ast/ExpressionNode.php index c007dc49..922f6255 100644 --- a/src/Parser/Ast/ExpressionNode.php +++ b/src/Parser/Ast/ExpressionNode.php @@ -23,27 +23,22 @@ namespace PackageFactory\ComponentEngine\Parser\Ast; use PackageFactory\ComponentEngine\Definition\Precedence; -use PackageFactory\ComponentEngine\Parser\Source\Source; +use PackageFactory\ComponentEngine\Parser\Parser\Expression\ExpressionParser; use PackageFactory\ComponentEngine\Parser\Tokenizer\LookAhead; use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Tokenizer; use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; final class ExpressionNode implements \JsonSerializable { - private function __construct( + public function __construct( public readonly IdentifierNode | NumberLiteralNode | BinaryOperationNode | UnaryOperationNode | AccessNode | TernaryOperationNode | TagNode | StringLiteralNode | MatchNode | TemplateLiteralNode | BooleanLiteralNode | NullLiteralNode $root ) { } public static function fromString(string $expressionAsString): self { - return self::fromTokens( - Tokenizer::fromSource( - Source::fromString($expressionAsString) - )->getIterator() - ); + return ExpressionParser::get()->tryString($expressionAsString)->output(); } /** @@ -122,7 +117,7 @@ public static function fromTokens(\Iterator $tokens, Precedence $precedence = Pr } Scanner::skipSpaceAndComments($tokens); - if (Scanner::isEnd($tokens) || $precedence->mustStopAt(Scanner::type($tokens))) { + if (Scanner::isEnd($tokens) || $precedence->mustStopAtTokenType(Scanner::type($tokens))) { return new self( root: $root ); diff --git a/src/Parser/Ast/IdentifierNode.php b/src/Parser/Ast/IdentifierNode.php index 5683bba0..2770efa6 100644 --- a/src/Parser/Ast/IdentifierNode.php +++ b/src/Parser/Ast/IdentifierNode.php @@ -22,6 +22,7 @@ namespace PackageFactory\ComponentEngine\Parser\Ast; +use PackageFactory\ComponentEngine\Parser\Parser\Identifier\IdentifierParser; use PackageFactory\ComponentEngine\Parser\Source\Source; use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; @@ -30,18 +31,14 @@ final class IdentifierNode implements \JsonSerializable { - private function __construct( + public function __construct( public readonly string $value ) { } public static function fromString(string $identifierAsString): self { - return self::fromTokens( - Tokenizer::fromSource( - Source::fromString($identifierAsString) - )->getIterator() - ); + return IdentifierParser::get()->tryString($identifierAsString)->output(); } /** diff --git a/src/Parser/Ast/NullLiteralNode.php b/src/Parser/Ast/NullLiteralNode.php index 15c2bb10..98f8e5dc 100644 --- a/src/Parser/Ast/NullLiteralNode.php +++ b/src/Parser/Ast/NullLiteralNode.php @@ -28,7 +28,7 @@ final class NullLiteralNode implements \JsonSerializable { - private function __construct() + public function __construct() { } diff --git a/src/Parser/Ast/NumberLiteralNode.php b/src/Parser/Ast/NumberLiteralNode.php index 49a81b17..7043847f 100644 --- a/src/Parser/Ast/NumberLiteralNode.php +++ b/src/Parser/Ast/NumberLiteralNode.php @@ -28,7 +28,7 @@ final class NumberLiteralNode implements \JsonSerializable { - private function __construct( + public function __construct( public readonly string $value, public readonly NumberFormat $format ) { diff --git a/src/Parser/Ast/StringLiteralNode.php b/src/Parser/Ast/StringLiteralNode.php index 53af12b4..38393638 100644 --- a/src/Parser/Ast/StringLiteralNode.php +++ b/src/Parser/Ast/StringLiteralNode.php @@ -22,6 +22,7 @@ namespace PackageFactory\ComponentEngine\Parser\Ast; +use PackageFactory\ComponentEngine\Parser\Parser\StringLiteral\StringLiteralParser; use PackageFactory\ComponentEngine\Parser\Source\Source; use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; @@ -30,18 +31,14 @@ final class StringLiteralNode implements \JsonSerializable { - private function __construct( + public function __construct( public readonly string $value ) { } public static function fromString(string $stringLiteralAsString): self { - return self::fromTokens( - Tokenizer::fromSource( - Source::fromString($stringLiteralAsString) - )->getIterator() - ); + return StringLiteralParser::get()->tryString($stringLiteralAsString)->output(); } /** diff --git a/src/Parser/Ast/TernaryOperationNode.php b/src/Parser/Ast/TernaryOperationNode.php index f5a8f35f..6e3c9827 100644 --- a/src/Parser/Ast/TernaryOperationNode.php +++ b/src/Parser/Ast/TernaryOperationNode.php @@ -29,7 +29,7 @@ final class TernaryOperationNode implements \JsonSerializable { - private function __construct( + public function __construct( public readonly ExpressionNode $condition, public readonly ExpressionNode $true, public readonly ExpressionNode $false diff --git a/src/Parser/Ast/UnaryOperationNode.php b/src/Parser/Ast/UnaryOperationNode.php index df8112ad..78387263 100644 --- a/src/Parser/Ast/UnaryOperationNode.php +++ b/src/Parser/Ast/UnaryOperationNode.php @@ -29,7 +29,7 @@ final class UnaryOperationNode implements \JsonSerializable { - private function __construct( + public function __construct( public readonly UnaryOperator $operator, public readonly ExpressionNode $argument ) { diff --git a/src/Parser/Parser/Access/AccessParser.php b/src/Parser/Parser/Access/AccessParser.php new file mode 100644 index 00000000..e18698fd --- /dev/null +++ b/src/Parser/Parser/Access/AccessParser.php @@ -0,0 +1,61 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Parser\Parser\Access; + +use PackageFactory\ComponentEngine\Definition\AccessType; +use PackageFactory\ComponentEngine\Parser\Ast\AccessChainSegmentNode; +use PackageFactory\ComponentEngine\Parser\Ast\AccessChainSegmentNodes; +use PackageFactory\ComponentEngine\Parser\Ast\AccessNode; +use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNode; +use PackageFactory\ComponentEngine\Parser\Parser\Identifier\IdentifierParser; +use Parsica\Parsica\Parser; + +use function Parsica\Parsica\atLeastOne; +use function Parsica\Parsica\char; +use function Parsica\Parsica\collect; +use function Parsica\Parsica\either; +use function Parsica\Parsica\string; + +final class AccessParser +{ + public static function get(ExpressionNode $subject): Parser + { + return atLeastOne( + collect( + self::accessType(), + IdentifierParser::get() + )->map(fn ($result) => [new AccessChainSegmentNode($result[0], $result[1])]) + )->map(fn ($segments) => new AccessNode( + $subject, + new AccessChainSegmentNodes(...$segments) + )); + } + + private static function accessType(): Parser + { + return either( + string('?.')->map(fn () => AccessType::OPTIONAL), + char('.')->map(fn () => AccessType::MANDATORY) + ); + } +} diff --git a/src/Parser/Parser/BinaryOperation/BinaryOperationParser.php b/src/Parser/Parser/BinaryOperation/BinaryOperationParser.php new file mode 100644 index 00000000..a60b64a0 --- /dev/null +++ b/src/Parser/Parser/BinaryOperation/BinaryOperationParser.php @@ -0,0 +1,79 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Parser\Parser\BinaryOperation; + +use PackageFactory\ComponentEngine\Definition\BinaryOperator; +use PackageFactory\ComponentEngine\Parser\Ast\BinaryOperationNode; +use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNode; +use PackageFactory\ComponentEngine\Parser\Parser\Expression\ExpressionParser; +use Parsica\Parsica\Internal\Succeed; +use Parsica\Parsica\Parser; + +use Parsica\Parsica\ParseResult; +use Parsica\Parsica\Stream; + +use function Parsica\Parsica\any; +use function Parsica\Parsica\char; +use function Parsica\Parsica\string; + +final class BinaryOperationParser +{ + public static function get(ExpressionNode $left): Parser + { + return Parser::make('binary operation', function (Stream $stream) use ($left): ParseResult { + $result = self::binaryOperatorParser()->run($stream); + if ($result->isFail()) { + return $result; + } + $binaryOperator = $result->output(); + assert($binaryOperator instanceof BinaryOperator); + $result = ExpressionParser::get($binaryOperator->toPrecedence())->continueFrom($result); + if ($result->isFail()) { + return $result; + } + return new Succeed( + new BinaryOperationNode($left, $binaryOperator, $result->output()), + $result->remainder() + ); + }); + } + + public static function binaryOperatorParser(): Parser + { + return any( + char('+')->map(fn () => BinaryOperator::PLUS), + char('-')->map(fn () => BinaryOperator::MINUS), + char('*')->map(fn () => BinaryOperator::MULTIPLY_BY), + char('/')->map(fn () => BinaryOperator::DIVIDE_BY), + char('%')->map(fn () => BinaryOperator::MODULO), + string('&&')->map(fn () => BinaryOperator::AND), + string('||')->map(fn () => BinaryOperator::OR), + string('>=')->map(fn () => BinaryOperator::GREATER_THAN_OR_EQUAL), + char('>')->map(fn () => BinaryOperator::GREATER_THAN), + string('<=')->map(fn () => BinaryOperator::LESS_THAN_OR_EQUAL), + char('<')->map(fn () => BinaryOperator::LESS_THAN), + string('===')->map(fn () => BinaryOperator::EQUAL), + string('!==')->map(fn () => BinaryOperator::NOT_EQUAL), + ); + } +} diff --git a/src/Parser/Parser/BooleanLiteral/BooleanLiteralParser.php b/src/Parser/Parser/BooleanLiteral/BooleanLiteralParser.php new file mode 100644 index 00000000..af0292fc --- /dev/null +++ b/src/Parser/Parser/BooleanLiteral/BooleanLiteralParser.php @@ -0,0 +1,46 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Parser\Parser\BooleanLiteral; + +use PackageFactory\ComponentEngine\Parser\Ast\BooleanLiteralNode; +use Parsica\Parsica\Parser; + +use function Parsica\Parsica\{either, skipSpace, string}; + +final class BooleanLiteralParser +{ + private static ?Parser $instance = null; + + public static function get(): Parser + { + return self::$instance ??= self::build(); + } + + private static function build(): Parser + { + return skipSpace()->sequence(either( + string('true')->map(fn () => new BooleanLiteralNode(true)), + string('false')->map(fn () => new BooleanLiteralNode(false)) + )); + } +} diff --git a/src/Parser/Parser/Expression/ExpressionParser.php b/src/Parser/Parser/Expression/ExpressionParser.php new file mode 100644 index 00000000..3b88c96e --- /dev/null +++ b/src/Parser/Parser/Expression/ExpressionParser.php @@ -0,0 +1,84 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Parser\Parser\Expression; + +use PackageFactory\ComponentEngine\Definition\Precedence; +use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNode; +use PackageFactory\ComponentEngine\Parser\Parser\Access\AccessParser; +use PackageFactory\ComponentEngine\Parser\Parser\BinaryOperation\BinaryOperationParser; +use PackageFactory\ComponentEngine\Parser\Parser\BooleanLiteral\BooleanLiteralParser; +use PackageFactory\ComponentEngine\Parser\Parser\Identifier\IdentifierParser; +use PackageFactory\ComponentEngine\Parser\Parser\NullLiteral\NullLiteralParser; +use PackageFactory\ComponentEngine\Parser\Parser\NumberLiteral\NumberLiteralParser; +use PackageFactory\ComponentEngine\Parser\Parser\StringLiteral\StringLiteralParser; +use PackageFactory\ComponentEngine\Parser\Parser\TernaryOperation\TernaryOperationParser; +use PackageFactory\ComponentEngine\Parser\Parser\UnaryOperation\UnaryOperationParser; +use Parsica\Parsica\Internal\Succeed; +use Parsica\Parsica\Parser; + +use Parsica\Parsica\ParseResult; +use Parsica\Parsica\Stream; + +use function Parsica\Parsica\{any, between, skipSpace}; + +final class ExpressionParser +{ + public static function get(Precedence $precedence = Precedence::SEQUENCE): Parser + { + return Parser::make('Expression', function (Stream $stream) use ($precedence): ParseResult { + $expressionRootParser = between(skipSpace(), skipSpace(), any( + NumberLiteralParser::get(), + BooleanLiteralParser::get(), + NullLiteralParser::get(), + StringLiteralParser::get(), + IdentifierParser::get(), + UnaryOperationParser::get() + )); + + $parseResult = $expressionRootParser->run($stream); + if ($parseResult->isFail()) { + return $parseResult; + } + $expressionNode = new ExpressionNode($parseResult->output()); + $remainder = $parseResult->remainder(); + + while ($parseResult->isSuccess() && !$precedence->mustStopAt(Precedence::fromRemainder($remainder))) { + $parseResult = any( + BinaryOperationParser::get($expressionNode), + AccessParser::get($expressionNode), + TernaryOperationParser::get($expressionNode), + )->thenIgnore(skipSpace())->continueFrom($parseResult); + + if ($parseResult->isSuccess()) { + $expressionNode = new ExpressionNode($parseResult->output()); + $remainder = $parseResult->remainder(); + } + } + + return new Succeed( + $expressionNode, + $remainder + ); + }); + } +} diff --git a/src/Parser/Parser/Identifier/IdentifierParser.php b/src/Parser/Parser/Identifier/IdentifierParser.php new file mode 100644 index 00000000..6398212e --- /dev/null +++ b/src/Parser/Parser/Identifier/IdentifierParser.php @@ -0,0 +1,44 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Parser\Parser\Identifier; + +use PackageFactory\ComponentEngine\Parser\Ast\IdentifierNode; +use Parsica\Parsica\Parser; + +use function Parsica\Parsica\{alphaChar, alphaNumChar, zeroOrMore}; + +final class IdentifierParser +{ + private static ?Parser $instance = null; + + public static function get(): Parser + { + return self::$instance ??= self::build(); + } + + private static function build(): Parser + { + return alphaChar()->append(zeroOrMore(alphaNumChar())) + ->map(fn ($name) => new IdentifierNode($name)); + } +} diff --git a/src/Parser/Parser/NullLiteral/NullLiteralParser.php b/src/Parser/Parser/NullLiteral/NullLiteralParser.php new file mode 100644 index 00000000..6a40cbee --- /dev/null +++ b/src/Parser/Parser/NullLiteral/NullLiteralParser.php @@ -0,0 +1,43 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Parser\Parser\NullLiteral; + +use PackageFactory\ComponentEngine\Parser\Ast\NullLiteralNode; +use Parsica\Parsica\Parser; + +use function Parsica\Parsica\{string}; + +final class NullLiteralParser +{ + private static ?Parser $instance = null; + + public static function get(): Parser + { + return self::$instance ??= self::build(); + } + + private static function build(): Parser + { + return string('null')->map(fn () => new NullLiteralNode()); + } +} diff --git a/src/Parser/Parser/NumberLiteral/NumberLiteralParser.php b/src/Parser/Parser/NumberLiteral/NumberLiteralParser.php new file mode 100644 index 00000000..a0075b41 --- /dev/null +++ b/src/Parser/Parser/NumberLiteral/NumberLiteralParser.php @@ -0,0 +1,49 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Parser\Parser\NumberLiteral; + +use PackageFactory\ComponentEngine\Definition\NumberFormat; +use PackageFactory\ComponentEngine\Parser\Ast\NumberLiteralNode; +use PackageFactory\ComponentEngine\Parser\Parser\NullLiteral\NullLiteralParser; +use Parsica\Parsica\Parser; + +use function Parsica\Parsica\alphaNumChar; +use function Parsica\Parsica\isDigit; +use function Parsica\Parsica\takeWhile; +use function Parsica\Parsica\takeWhile1; + +final class NumberLiteralParser +{ + private static ?Parser $instance = null; + + public static function get(): Parser + { + return self::$instance ??= self::build(); + } + + private static function build(): Parser + { + return takeWhile1(isDigit()) + ->map(fn ($value) => new NumberLiteralNode($value, NumberFormat::DECIMAL)); + } +} diff --git a/src/Parser/Parser/StringLiteral/StringLiteralParser.php b/src/Parser/Parser/StringLiteral/StringLiteralParser.php new file mode 100644 index 00000000..bc82170e --- /dev/null +++ b/src/Parser/Parser/StringLiteral/StringLiteralParser.php @@ -0,0 +1,67 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Parser\Parser\StringLiteral; + +use PackageFactory\ComponentEngine\Parser\Ast\StringLiteralNode; +use Parsica\Parsica\Parser; + +use function Parsica\Parsica\{anySingle, append, between, char, assemble, either, takeWhile, zeroOrMore}; + +final class StringLiteralParser +{ + private static ?Parser $instance = null; + + public static function get(): Parser + { + return self::$instance ??= self::build(); + } + + private static function build(): Parser + { + return either( + self::forQuotedStringType('\''), + self::forQuotedStringType('"') + )->map(fn (string $contents) => new StringLiteralNode($contents)); + } + + private static function forQuotedStringType(string $qouteType): Parser + { + assert($qouteType === '"' || $qouteType === '\''); + $takeAllNonBackslashesAndQuoteChars = takeWhile( + fn (string $char): bool => $char !== $qouteType && $char !== '\\' + ); + return between( + char($qouteType), + char($qouteType), + append( + $takeAllNonBackslashesAndQuoteChars, + zeroOrMore( + append( + char("\\")->followedBy(anySingle()), + $takeAllNonBackslashesAndQuoteChars, + ) + ) + ) + ); + } +} diff --git a/src/Parser/Parser/TernaryOperation/TernaryOperationParser.php b/src/Parser/Parser/TernaryOperation/TernaryOperationParser.php new file mode 100644 index 00000000..b4af36a2 --- /dev/null +++ b/src/Parser/Parser/TernaryOperation/TernaryOperationParser.php @@ -0,0 +1,43 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Parser\Parser\TernaryOperation; + +use PackageFactory\ComponentEngine\Definition\Precedence; +use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNode; +use PackageFactory\ComponentEngine\Parser\Ast\TernaryOperationNode; +use PackageFactory\ComponentEngine\Parser\Parser\Expression\ExpressionParser; +use Parsica\Parsica\Parser; + +use function Parsica\Parsica\char; +use function Parsica\Parsica\collect; + +final class TernaryOperationParser +{ + public static function get(ExpressionNode $condition): Parser + { + return collect( + char('?')->sequence(ExpressionParser::get(Precedence::TERNARY)), + char(':')->sequence(ExpressionParser::get(Precedence::TERNARY)) + )->map(fn ($branches) => new TernaryOperationNode($condition, $branches[0], $branches[1])); + } +} diff --git a/src/Parser/Parser/UnaryOperation/UnaryOperationParser.php b/src/Parser/Parser/UnaryOperation/UnaryOperationParser.php new file mode 100644 index 00000000..35155229 --- /dev/null +++ b/src/Parser/Parser/UnaryOperation/UnaryOperationParser.php @@ -0,0 +1,55 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Parser\Parser\UnaryOperation; + +use PackageFactory\ComponentEngine\Definition\Precedence; +use PackageFactory\ComponentEngine\Definition\UnaryOperator; +use PackageFactory\ComponentEngine\Parser\Ast\UnaryOperationNode; +use PackageFactory\ComponentEngine\Parser\Parser\Expression\ExpressionParser; +use Parsica\Parsica\Parser; + +use function Parsica\Parsica\char; +use function Parsica\Parsica\collect; + +final class UnaryOperationParser +{ + private static ?Parser $instance = null; + + public static function get(): Parser + { + return self::$instance ??= self::build(); + } + + private static function build(): Parser + { + return collect( + self::unaryOperator(), + ExpressionParser::get(Precedence::UNARY) + )->map(fn ($result) => new UnaryOperationNode($result[0], $result[1])); + } + + private static function unaryOperator(): Parser + { + return char('!')->map(fn () => UnaryOperator::NOT); + } +} From a3ef1fcde4c97c2ec1d75302d405d6d41927aa25 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Thu, 18 May 2023 23:20:08 +0200 Subject: [PATCH 02/20] WIP: Parsica use bind instead of Parser::make --- src/Definition/Precedence.php | 15 ++++- src/Definition/UnaryOperator.php | 7 ++ .../BinaryOperation/BinaryOperationParser.php | 21 ++---- .../Parser/Expression/ExpressionParser.php | 64 +++++++++---------- .../UnaryOperation/UnaryOperationParser.php | 7 +- 5 files changed, 60 insertions(+), 54 deletions(-) diff --git a/src/Definition/Precedence.php b/src/Definition/Precedence.php index 7ab95494..b4437307 100644 --- a/src/Definition/Precedence.php +++ b/src/Definition/Precedence.php @@ -23,8 +23,11 @@ namespace PackageFactory\ComponentEngine\Definition; use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; +use Parsica\Parsica\Parser; +use Parsica\Parsica\ParseResult; use Parsica\Parsica\Stream; +use function Parsica\Parsica\fail; enum Precedence: int { @@ -94,7 +97,17 @@ public static function forTokenType(TokenType $tokenType): self }; } - public static function fromRemainder(Stream $stream): self + public function delegate(Parser $parser): Parser + { + return Parser::make('delegate in Precedence(' . $this->name . ')', function (Stream $stream) use($parser): ParseResult { + if ($this->mustStopAt(self::fromRemainder($stream))) { + return fail('')->run($stream); + } + return $parser->run($stream); + }); + } + + private static function fromRemainder(Stream $stream): self { // @todo dont rely on `__toString` as its an implementation detail and the stream could be lazy $contents = $stream->__toString(); diff --git a/src/Definition/UnaryOperator.php b/src/Definition/UnaryOperator.php index 2e7c6f32..611f3b07 100644 --- a/src/Definition/UnaryOperator.php +++ b/src/Definition/UnaryOperator.php @@ -35,4 +35,11 @@ public static function fromTokenType(TokenType $tokenType): self default => throw new \Exception('@TODO: Unknown Unary Operator') }; } + + public function toPrecedence(): Precedence + { + return match ($this) { + self::NOT => Precedence::UNARY + }; + } } diff --git a/src/Parser/Parser/BinaryOperation/BinaryOperationParser.php b/src/Parser/Parser/BinaryOperation/BinaryOperationParser.php index a60b64a0..d5d3d046 100644 --- a/src/Parser/Parser/BinaryOperation/BinaryOperationParser.php +++ b/src/Parser/Parser/BinaryOperation/BinaryOperationParser.php @@ -40,21 +40,12 @@ final class BinaryOperationParser { public static function get(ExpressionNode $left): Parser { - return Parser::make('binary operation', function (Stream $stream) use ($left): ParseResult { - $result = self::binaryOperatorParser()->run($stream); - if ($result->isFail()) { - return $result; - } - $binaryOperator = $result->output(); - assert($binaryOperator instanceof BinaryOperator); - $result = ExpressionParser::get($binaryOperator->toPrecedence())->continueFrom($result); - if ($result->isFail()) { - return $result; - } - return new Succeed( - new BinaryOperationNode($left, $binaryOperator, $result->output()), - $result->remainder() - ); + return self::binaryOperatorParser()->bind(function (BinaryOperator $binaryOperator) use ($left) { + return ExpressionParser::get($binaryOperator->toPrecedence())->map(fn ($right) => new BinaryOperationNode( + $left, + $binaryOperator, + $right + )); }); } diff --git a/src/Parser/Parser/Expression/ExpressionParser.php b/src/Parser/Parser/Expression/ExpressionParser.php index 3b88c96e..0e505f68 100644 --- a/src/Parser/Parser/Expression/ExpressionParser.php +++ b/src/Parser/Parser/Expression/ExpressionParser.php @@ -35,50 +35,46 @@ use PackageFactory\ComponentEngine\Parser\Parser\UnaryOperation\UnaryOperationParser; use Parsica\Parsica\Internal\Succeed; use Parsica\Parsica\Parser; - use Parsica\Parsica\ParseResult; use Parsica\Parsica\Stream; -use function Parsica\Parsica\{any, between, skipSpace}; +use function Parsica\Parsica\{any, between, either, pure, skipSpace}; final class ExpressionParser { public static function get(Precedence $precedence = Precedence::SEQUENCE): Parser { - return Parser::make('Expression', function (Stream $stream) use ($precedence): ParseResult { - $expressionRootParser = between(skipSpace(), skipSpace(), any( - NumberLiteralParser::get(), - BooleanLiteralParser::get(), - NullLiteralParser::get(), - StringLiteralParser::get(), - IdentifierParser::get(), - UnaryOperationParser::get() - )); - - $parseResult = $expressionRootParser->run($stream); - if ($parseResult->isFail()) { - return $parseResult; - } - $expressionNode = new ExpressionNode($parseResult->output()); - $remainder = $parseResult->remainder(); + $expressionRootParser = between(skipSpace(), skipSpace(), any( + NumberLiteralParser::get(), + BooleanLiteralParser::get(), + NullLiteralParser::get(), + StringLiteralParser::get(), + IdentifierParser::get(), + UnaryOperationParser::get() + )); - while ($parseResult->isSuccess() && !$precedence->mustStopAt(Precedence::fromRemainder($remainder))) { - $parseResult = any( - BinaryOperationParser::get($expressionNode), - AccessParser::get($expressionNode), - TernaryOperationParser::get($expressionNode), - )->thenIgnore(skipSpace())->continueFrom($parseResult); + return $expressionRootParser->map(fn ($expressionRoot) => new ExpressionNode($expressionRoot)) + ->bind(function ($expressionNode) use ($precedence) { + return self::continueParsing($expressionNode, $precedence); + }); + } - if ($parseResult->isSuccess()) { - $expressionNode = new ExpressionNode($parseResult->output()); - $remainder = $parseResult->remainder(); - } - } + private static function continueParsing(ExpressionNode $expressionNode, Precedence $precedence): Parser + { + $continuationParsers = any( + BinaryOperationParser::get($expressionNode), + AccessParser::get($expressionNode), + TernaryOperationParser::get($expressionNode) + ) + ->thenIgnore(skipSpace()) + ->bind(function ($expressionRoot) use ($precedence) { + $newExpressionNode = new ExpressionNode($expressionRoot); + return self::continueParsing($newExpressionNode, $precedence); + }); - return new Succeed( - $expressionNode, - $remainder - ); - }); + return either( + $precedence->delegate($continuationParsers), + pure($expressionNode) + ); } } diff --git a/src/Parser/Parser/UnaryOperation/UnaryOperationParser.php b/src/Parser/Parser/UnaryOperation/UnaryOperationParser.php index 35155229..3e29f746 100644 --- a/src/Parser/Parser/UnaryOperation/UnaryOperationParser.php +++ b/src/Parser/Parser/UnaryOperation/UnaryOperationParser.php @@ -42,10 +42,9 @@ public static function get(): Parser private static function build(): Parser { - return collect( - self::unaryOperator(), - ExpressionParser::get(Precedence::UNARY) - )->map(fn ($result) => new UnaryOperationNode($result[0], $result[1])); + return self::unaryOperator()->bind(function (UnaryOperator $unaryOperator) { + return ExpressionParser::get($unaryOperator->toPrecedence())->map(fn ($expression) => new UnaryOperationNode($unaryOperator, $expression)); + }); } private static function unaryOperator(): Parser From 6f94ba36c40f6e91d3157c775b4760cc8e02303c Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Fri, 19 May 2023 09:14:37 +0200 Subject: [PATCH 03/20] WIP: Parsica rewrite Precedence to only use @api 50% slower due to `getPrecedenceMatcher` before: 23ms now: 53ms --- src/Definition/Precedence.php | 42 +++++++++++-------- .../Parser/Expression/ExpressionParser.php | 3 +- 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/src/Definition/Precedence.php b/src/Definition/Precedence.php index b4437307..e5871032 100644 --- a/src/Definition/Precedence.php +++ b/src/Definition/Precedence.php @@ -24,10 +24,13 @@ use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; use Parsica\Parsica\Parser; -use Parsica\Parsica\ParseResult; -use Parsica\Parsica\Stream; +use function Parsica\Parsica\any; use function Parsica\Parsica\fail; +use function Parsica\Parsica\lookAhead; +use function Parsica\Parsica\pure; +use function Parsica\Parsica\string; +use function Parsica\Parsica\succeed; enum Precedence: int { @@ -97,28 +100,31 @@ public static function forTokenType(TokenType $tokenType): self }; } - public function delegate(Parser $parser): Parser + private static function getPrecedenceMatcher(): Parser { - return Parser::make('delegate in Precedence(' . $this->name . ')', function (Stream $stream) use($parser): ParseResult { - if ($this->mustStopAt(self::fromRemainder($stream))) { - return fail('')->run($stream); + static $matcher; + if ($matcher) { + return $matcher; + } + $parsers = []; + foreach (self::ARRAY_OF_STRINGS_TO_PRECEDENCE as [$strings, $precedence]) { + $toPrecedence = fn () => $precedence; + foreach ($strings as $string) { + $parsers[] = string($string)->map($toPrecedence); } - return $parser->run($stream); - }); + } + $matcher = any(...$parsers, ...[pure(self::SEQUENCE)]); + return $matcher; } - private static function fromRemainder(Stream $stream): self + public function check(): Parser { - // @todo dont rely on `__toString` as its an implementation detail and the stream could be lazy - $contents = $stream->__toString(); - foreach (self::ARRAY_OF_STRINGS_TO_PRECEDENCE as [$strings, $pre]) { - foreach ($strings as $string) { - if (str_starts_with($contents, $string)) { - return $pre; - } + return lookAhead(self::getPrecedenceMatcher()->bind(function (Precedence $precedence) { + if ($this->mustStopAt($precedence)) { + return fail(''); } - } - return self::SEQUENCE; + return succeed(); + }))->label('delegate in Precedence(' . $this->name . ')'); } public function mustStopAt(self $precedence): bool diff --git a/src/Parser/Parser/Expression/ExpressionParser.php b/src/Parser/Parser/Expression/ExpressionParser.php index 0e505f68..b30ba506 100644 --- a/src/Parser/Parser/Expression/ExpressionParser.php +++ b/src/Parser/Parser/Expression/ExpressionParser.php @@ -73,7 +73,8 @@ private static function continueParsing(ExpressionNode $expressionNode, Preceden }); return either( - $precedence->delegate($continuationParsers), + $precedence->check() + ->sequence($continuationParsers), pure($expressionNode) ); } From 635418469522f0a4a7721c7f14b7560f8348a765 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Fri, 19 May 2023 09:50:37 +0200 Subject: [PATCH 04/20] WIP: Parsica speed up Precedence via custom strings parser back to 23ms ;) --- src/Definition/Precedence.php | 66 ++++++++++++++++++++++++++++++----- 1 file changed, 58 insertions(+), 8 deletions(-) diff --git a/src/Definition/Precedence.php b/src/Definition/Precedence.php index e5871032..ccc31e95 100644 --- a/src/Definition/Precedence.php +++ b/src/Definition/Precedence.php @@ -23,13 +23,17 @@ namespace PackageFactory\ComponentEngine\Definition; use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; +use Parsica\Parsica\Internal\Fail; +use Parsica\Parsica\Internal\Succeed; use Parsica\Parsica\Parser; +use Parsica\Parsica\ParseResult; +use Parsica\Parsica\Stream; -use function Parsica\Parsica\any; +use function Parsica\Parsica\either; use function Parsica\Parsica\fail; use function Parsica\Parsica\lookAhead; +use function Parsica\Parsica\oneOf; use function Parsica\Parsica\pure; -use function Parsica\Parsica\string; use function Parsica\Parsica\succeed; enum Precedence: int @@ -100,31 +104,77 @@ public static function forTokenType(TokenType $tokenType): self }; } + /** + * A multi character compatible version (eg. for strings) of {@see oneOf()} + * + * While one could leverage multiple string parsers, it's not really performance efficient: + * + * any(string('f'), string('bar')) + * + * the above can be rewritten like: + * + * strings(['f', 'bar']) + * + * @param array $strings + * @return Parser + */ + private static function strings(array $strings): Parser + { + $longestString = 0; + foreach ($strings as $string) { + $len = mb_strlen($string); + if ($longestString < $len) { + $longestString = $len; + } + } + return Parser::make('strings', function (Stream $stream) use($strings, $longestString): ParseResult { + if ($stream->isEOF()) { + return new Fail('strings', $stream); + } + $result = $stream->takeN($longestString); + foreach ($strings as $string) { + if (str_starts_with($result->chunk(), $string)) { + return new Succeed($string, $stream->takeN(mb_strlen($string))->stream()); + } + } + return new Fail('strings', $stream); + }); + } + private static function getPrecedenceMatcher(): Parser { static $matcher; if ($matcher) { return $matcher; } - $parsers = []; + $allStrings = []; + $stringToPrecedence = []; foreach (self::ARRAY_OF_STRINGS_TO_PRECEDENCE as [$strings, $precedence]) { - $toPrecedence = fn () => $precedence; foreach ($strings as $string) { - $parsers[] = string($string)->map($toPrecedence); + $allStrings[] = $string; + $stringToPrecedence[$string] = $precedence; } } - $matcher = any(...$parsers, ...[pure(self::SEQUENCE)]); + $matcher = + either( + lookAhead( + self::strings($allStrings)->map(function ($match) use($stringToPrecedence) { + return $stringToPrecedence[$match]; + }) + ), + pure(self::SEQUENCE) + ); return $matcher; } public function check(): Parser { - return lookAhead(self::getPrecedenceMatcher()->bind(function (Precedence $precedence) { + return self::getPrecedenceMatcher()->bind(function (Precedence $precedence) { if ($this->mustStopAt($precedence)) { return fail(''); } return succeed(); - }))->label('delegate in Precedence(' . $this->name . ')'); + })->label('check Precedence(' . $this->name . ')'); } public function mustStopAt(self $precedence): bool From 41b94c79232f0c023bad0ccefadbc4013f17e2ba Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Fri, 19 May 2023 11:16:21 +0200 Subject: [PATCH 05/20] WIP: Parsica add MatchParser --- src/Parser/Ast/ExpressionNodes.php | 2 +- src/Parser/Ast/MatchArmNode.php | 2 +- src/Parser/Ast/MatchArmNodes.php | 2 +- src/Parser/Ast/MatchNode.php | 2 +- .../Parser/Expression/ExpressionParser.php | 6 +- src/Parser/Parser/Match/MatchParser.php | 88 +++++++++++++++++++ 6 files changed, 95 insertions(+), 7 deletions(-) create mode 100644 src/Parser/Parser/Match/MatchParser.php diff --git a/src/Parser/Ast/ExpressionNodes.php b/src/Parser/Ast/ExpressionNodes.php index e16b0e75..83eaee4f 100644 --- a/src/Parser/Ast/ExpressionNodes.php +++ b/src/Parser/Ast/ExpressionNodes.php @@ -33,7 +33,7 @@ final class ExpressionNodes implements \JsonSerializable */ public readonly array $items; - private function __construct( + public function __construct( ExpressionNode ...$items ) { $this->items = $items; diff --git a/src/Parser/Ast/MatchArmNode.php b/src/Parser/Ast/MatchArmNode.php index 5d96a814..705169b2 100644 --- a/src/Parser/Ast/MatchArmNode.php +++ b/src/Parser/Ast/MatchArmNode.php @@ -28,7 +28,7 @@ final class MatchArmNode implements \JsonSerializable { - private function __construct( + public function __construct( public readonly null | ExpressionNodes $left, public readonly ExpressionNode $right ) { diff --git a/src/Parser/Ast/MatchArmNodes.php b/src/Parser/Ast/MatchArmNodes.php index 6c750048..500cf4d1 100644 --- a/src/Parser/Ast/MatchArmNodes.php +++ b/src/Parser/Ast/MatchArmNodes.php @@ -33,7 +33,7 @@ final class MatchArmNodes implements \JsonSerializable */ public readonly array $items; - private function __construct( + public function __construct( MatchArmNode ...$items ) { $this->items = $items; diff --git a/src/Parser/Ast/MatchNode.php b/src/Parser/Ast/MatchNode.php index b975a020..b044b07b 100644 --- a/src/Parser/Ast/MatchNode.php +++ b/src/Parser/Ast/MatchNode.php @@ -28,7 +28,7 @@ final class MatchNode implements \JsonSerializable { - private function __construct( + public function __construct( public readonly ExpressionNode $subject, public readonly MatchArmNodes $arms ) { diff --git a/src/Parser/Parser/Expression/ExpressionParser.php b/src/Parser/Parser/Expression/ExpressionParser.php index b30ba506..108c9e75 100644 --- a/src/Parser/Parser/Expression/ExpressionParser.php +++ b/src/Parser/Parser/Expression/ExpressionParser.php @@ -28,15 +28,13 @@ use PackageFactory\ComponentEngine\Parser\Parser\BinaryOperation\BinaryOperationParser; use PackageFactory\ComponentEngine\Parser\Parser\BooleanLiteral\BooleanLiteralParser; use PackageFactory\ComponentEngine\Parser\Parser\Identifier\IdentifierParser; +use PackageFactory\ComponentEngine\Parser\Parser\Match\MatchParser; use PackageFactory\ComponentEngine\Parser\Parser\NullLiteral\NullLiteralParser; use PackageFactory\ComponentEngine\Parser\Parser\NumberLiteral\NumberLiteralParser; use PackageFactory\ComponentEngine\Parser\Parser\StringLiteral\StringLiteralParser; use PackageFactory\ComponentEngine\Parser\Parser\TernaryOperation\TernaryOperationParser; use PackageFactory\ComponentEngine\Parser\Parser\UnaryOperation\UnaryOperationParser; -use Parsica\Parsica\Internal\Succeed; use Parsica\Parsica\Parser; -use Parsica\Parsica\ParseResult; -use Parsica\Parsica\Stream; use function Parsica\Parsica\{any, between, either, pure, skipSpace}; @@ -48,6 +46,7 @@ public static function get(Precedence $precedence = Precedence::SEQUENCE): Parse NumberLiteralParser::get(), BooleanLiteralParser::get(), NullLiteralParser::get(), + MatchParser::get(), StringLiteralParser::get(), IdentifierParser::get(), UnaryOperationParser::get() @@ -79,3 +78,4 @@ private static function continueParsing(ExpressionNode $expressionNode, Preceden ); } } + diff --git a/src/Parser/Parser/Match/MatchParser.php b/src/Parser/Parser/Match/MatchParser.php new file mode 100644 index 00000000..7856ab79 --- /dev/null +++ b/src/Parser/Parser/Match/MatchParser.php @@ -0,0 +1,88 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Parser\Parser\Match; + +use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNode; +use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNodes; +use PackageFactory\ComponentEngine\Parser\Ast\MatchArmNode; +use PackageFactory\ComponentEngine\Parser\Ast\MatchArmNodes; +use PackageFactory\ComponentEngine\Parser\Ast\MatchNode; +use PackageFactory\ComponentEngine\Parser\Parser\Expression\ExpressionParser; +use Parsica\Parsica\Parser; + +use function Parsica\Parsica\char; +use function Parsica\Parsica\collect; +use function Parsica\Parsica\either; +use function Parsica\Parsica\skipSpace; +use function Parsica\Parsica\string; +use function Parsica\Parsica\zeroOrMore; + +final class MatchParser +{ + private static ?Parser $instance = null; + + public static function get(): Parser + { + return self::$instance ??= self::build(); + } + + private static function build(): Parser + { + // @todo for some reason we must use bind here to avoid infinite recursion + return string('match') + ->bind(fn () => skipSpace() + ->followedBy(char('(')) + ->followedBy(ExpressionParser::get()) + ->thenIgnore(char(')')) + ->thenIgnore(skipSpace()) + ->thenIgnore(char('{')) + ->thenIgnore(skipSpace()) + ->bind(fn ($expression) => self::getMatchArmsParser()->map(fn ($matchArmNodes) => new MatchNode($expression, $matchArmNodes))) + ->thenIgnore(skipSpace()) + ->thenIgnore(char('}')) + ); + } + + private static function getMatchArmParser(): Parser + { + return collect( + either( + string('default')->map(fn () => null), + ExpressionParser::get()->map(fn (ExpressionNode $expressionNode) => new ExpressionNodes($expressionNode)) + ), + skipSpace(), + string('->'), + skipSpace(), + ExpressionParser::get(), + skipSpace(), + )->map(fn ($collected) => new MatchArmNode($collected[0], $collected[4])); + + } + + private static function getMatchArmsParser(): Parser + { + return zeroOrMore( + self::getMatchArmParser()->map(fn ($matchArmNode) => [$matchArmNode]) + )->map(fn ($matchArmNodes) => new MatchArmNodes(...$matchArmNodes ? $matchArmNodes : [])); + } +} From 590a5f1526b78d7e430e568fa820041ffef7b997 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Fri, 19 May 2023 11:40:27 +0200 Subject: [PATCH 06/20] WIP: Parsica NumberLiteralParser support for funny number formats --- .../NumberLiteral/NumberLiteralParser.php | 56 ++++++++++++++++++- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/src/Parser/Parser/NumberLiteral/NumberLiteralParser.php b/src/Parser/Parser/NumberLiteral/NumberLiteralParser.php index a0075b41..cafdd724 100644 --- a/src/Parser/Parser/NumberLiteral/NumberLiteralParser.php +++ b/src/Parser/Parser/NumberLiteral/NumberLiteralParser.php @@ -24,11 +24,21 @@ use PackageFactory\ComponentEngine\Definition\NumberFormat; use PackageFactory\ComponentEngine\Parser\Ast\NumberLiteralNode; -use PackageFactory\ComponentEngine\Parser\Parser\NullLiteral\NullLiteralParser; use Parsica\Parsica\Parser; -use function Parsica\Parsica\alphaNumChar; +use function Parsica\Parsica\any; +use function Parsica\Parsica\append; +use function Parsica\Parsica\assemble; +use function Parsica\Parsica\char; +use function Parsica\Parsica\charI; +use function Parsica\Parsica\either; +use function Parsica\Parsica\float; +use function Parsica\Parsica\integer; +use function Parsica\Parsica\isCharCode; use function Parsica\Parsica\isDigit; +use function Parsica\Parsica\isHexDigit; +use function Parsica\Parsica\optional; +use function Parsica\Parsica\stringI; use function Parsica\Parsica\takeWhile; use function Parsica\Parsica\takeWhile1; @@ -43,7 +53,47 @@ public static function get(): Parser private static function build(): Parser { - return takeWhile1(isDigit()) + $isOctalDigit = isCharCode(range(0x30, 0x37)); + $isBinaryDigit = isCharCode([0x30, 0x31]); + + $binaryParser = stringI('0b')->append(takeWhile($isBinaryDigit)) + ->map(fn ($value) => new NumberLiteralNode($value, NumberFormat::BINARY)); + + $octalParser = stringI('0o')->append(takeWhile($isOctalDigit)) + ->map(fn ($value) => new NumberLiteralNode($value, NumberFormat::OCTAL)); + + $hexadecimalParser = stringI('0x')->append(takeWhile(isHexDigit())) + ->map(fn ($value) => new NumberLiteralNode($value, NumberFormat::HEXADECIMAL)); + + $decimalParser = self::float() ->map(fn ($value) => new NumberLiteralNode($value, NumberFormat::DECIMAL)); + + return any( + $binaryParser, + $octalParser, + $hexadecimalParser, + $decimalParser, + ); + } + + /** + * adjusted from {@see float()} + */ + private static function float(): Parser + { + $digits = takeWhile1(isDigit())->label('at least one 0-9'); + $fraction = char('.')->append($digits); + $exponent = charI('e')->append($digits); + return either( + assemble( + integer(), + optional($fraction), + optional($exponent) + ), + append( + $fraction, + optional($exponent) + ) + )->label("float"); } } From c58a9c9deb0b643b4211e65d92c53e3548f867db Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Fri, 19 May 2023 12:42:25 +0200 Subject: [PATCH 07/20] WIP: Parsica TagNode Parsing --- src/Parser/Ast/AttributeNodes.php | 2 +- src/Parser/Ast/TagContentNode.php | 2 +- src/Parser/Ast/TagContentNodes.php | 2 +- src/Parser/Ast/TagNode.php | 2 +- src/Parser/Ast/TextNode.php | 2 +- .../Parser/Attribute/AttributeParser.php | 42 ++++++++++ .../Parser/Expression/ExpressionParser.php | 2 + .../NumberLiteral/NumberLiteralParser.php | 3 +- .../StringLiteral/StringLiteralParser.php | 2 + src/Parser/Parser/Tag/TagParser.php | 82 +++++++++++++++++++ .../Parser/TagContent/TagContentParser.php | 60 ++++++++++++++ src/Parser/Parser/Text/TextParser.php | 44 ++++++++++ 12 files changed, 238 insertions(+), 7 deletions(-) create mode 100644 src/Parser/Parser/Attribute/AttributeParser.php create mode 100644 src/Parser/Parser/Tag/TagParser.php create mode 100644 src/Parser/Parser/TagContent/TagContentParser.php create mode 100644 src/Parser/Parser/Text/TextParser.php diff --git a/src/Parser/Ast/AttributeNodes.php b/src/Parser/Ast/AttributeNodes.php index b829373c..d356c0b7 100644 --- a/src/Parser/Ast/AttributeNodes.php +++ b/src/Parser/Ast/AttributeNodes.php @@ -33,7 +33,7 @@ final class AttributeNodes implements \JsonSerializable */ public readonly array $items; - private function __construct( + public function __construct( AttributeNode ...$items ) { $itemsAsHashMap = []; diff --git a/src/Parser/Ast/TagContentNode.php b/src/Parser/Ast/TagContentNode.php index 31097089..5986eb54 100644 --- a/src/Parser/Ast/TagContentNode.php +++ b/src/Parser/Ast/TagContentNode.php @@ -28,7 +28,7 @@ final class TagContentNode implements \JsonSerializable { - private function __construct( + public function __construct( public readonly TextNode|ExpressionNode|TagNode $root ) { } diff --git a/src/Parser/Ast/TagContentNodes.php b/src/Parser/Ast/TagContentNodes.php index b04a0355..b9b154fd 100644 --- a/src/Parser/Ast/TagContentNodes.php +++ b/src/Parser/Ast/TagContentNodes.php @@ -33,7 +33,7 @@ final class TagContentNodes implements \JsonSerializable */ public readonly array $items; - private function __construct( + public function __construct( TagContentNode ...$items ) { $this->items = $items; diff --git a/src/Parser/Ast/TagNode.php b/src/Parser/Ast/TagNode.php index 317a466c..f6346184 100644 --- a/src/Parser/Ast/TagNode.php +++ b/src/Parser/Ast/TagNode.php @@ -28,7 +28,7 @@ final class TagNode implements \JsonSerializable { - private function __construct( + public function __construct( public readonly string $tagName, public readonly AttributeNodes $attributes, public readonly TagContentNodes $children, diff --git a/src/Parser/Ast/TextNode.php b/src/Parser/Ast/TextNode.php index 3a13fc5b..51386892 100644 --- a/src/Parser/Ast/TextNode.php +++ b/src/Parser/Ast/TextNode.php @@ -30,7 +30,7 @@ final class TextNode implements \JsonSerializable { - private function __construct( + public function __construct( public readonly string $value ) { } diff --git a/src/Parser/Parser/Attribute/AttributeParser.php b/src/Parser/Parser/Attribute/AttributeParser.php new file mode 100644 index 00000000..3f0d7a66 --- /dev/null +++ b/src/Parser/Parser/Attribute/AttributeParser.php @@ -0,0 +1,42 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Parser\Parser\Attribute; + +use PackageFactory\ComponentEngine\Parser\Ast\AttributeNodes; +use Parsica\Parsica\Parser; + +use function Parsica\Parsica\succeed; +final class AttributeParser +{ + private static ?Parser $instance = null; + + public static function get(): Parser + { + return self::$instance ??= self::build(); + } + + private static function build(): Parser + { + return succeed()->map(fn () => new AttributeNodes()); + } +} diff --git a/src/Parser/Parser/Expression/ExpressionParser.php b/src/Parser/Parser/Expression/ExpressionParser.php index 108c9e75..3b08d5cd 100644 --- a/src/Parser/Parser/Expression/ExpressionParser.php +++ b/src/Parser/Parser/Expression/ExpressionParser.php @@ -32,6 +32,7 @@ use PackageFactory\ComponentEngine\Parser\Parser\NullLiteral\NullLiteralParser; use PackageFactory\ComponentEngine\Parser\Parser\NumberLiteral\NumberLiteralParser; use PackageFactory\ComponentEngine\Parser\Parser\StringLiteral\StringLiteralParser; +use PackageFactory\ComponentEngine\Parser\Parser\Tag\TagParser; use PackageFactory\ComponentEngine\Parser\Parser\TernaryOperation\TernaryOperationParser; use PackageFactory\ComponentEngine\Parser\Parser\UnaryOperation\UnaryOperationParser; use Parsica\Parsica\Parser; @@ -47,6 +48,7 @@ public static function get(Precedence $precedence = Precedence::SEQUENCE): Parse BooleanLiteralParser::get(), NullLiteralParser::get(), MatchParser::get(), + TagParser::get(), StringLiteralParser::get(), IdentifierParser::get(), UnaryOperationParser::get() diff --git a/src/Parser/Parser/NumberLiteral/NumberLiteralParser.php b/src/Parser/Parser/NumberLiteral/NumberLiteralParser.php index cafdd724..d45556ba 100644 --- a/src/Parser/Parser/NumberLiteral/NumberLiteralParser.php +++ b/src/Parser/Parser/NumberLiteral/NumberLiteralParser.php @@ -33,7 +33,6 @@ use function Parsica\Parsica\charI; use function Parsica\Parsica\either; use function Parsica\Parsica\float; -use function Parsica\Parsica\integer; use function Parsica\Parsica\isCharCode; use function Parsica\Parsica\isDigit; use function Parsica\Parsica\isHexDigit; @@ -86,7 +85,7 @@ private static function float(): Parser $exponent = charI('e')->append($digits); return either( assemble( - integer(), + $digits, optional($fraction), optional($exponent) ), diff --git a/src/Parser/Parser/StringLiteral/StringLiteralParser.php b/src/Parser/Parser/StringLiteral/StringLiteralParser.php index bc82170e..0de48890 100644 --- a/src/Parser/Parser/StringLiteral/StringLiteralParser.php +++ b/src/Parser/Parser/StringLiteral/StringLiteralParser.php @@ -38,6 +38,8 @@ public static function get(): Parser private static function build(): Parser { + // labels, and escaping handling + // also maybe use bind with the start character instead of two parsers return either( self::forQuotedStringType('\''), self::forQuotedStringType('"') diff --git a/src/Parser/Parser/Tag/TagParser.php b/src/Parser/Parser/Tag/TagParser.php new file mode 100644 index 00000000..6488798a --- /dev/null +++ b/src/Parser/Parser/Tag/TagParser.php @@ -0,0 +1,82 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Parser\Parser\Tag; + +use PackageFactory\ComponentEngine\Parser\Ast\TagContentNodes; +use PackageFactory\ComponentEngine\Parser\Ast\TagNode; +use PackageFactory\ComponentEngine\Parser\Parser\Attribute\AttributeParser; +use PackageFactory\ComponentEngine\Parser\Parser\TagContent\TagContentParser; +use Parsica\Parsica\Parser; + +use function Parsica\Parsica\alphaChar; +use function Parsica\Parsica\assemble; +use function Parsica\Parsica\char; +use function Parsica\Parsica\either; +use function Parsica\Parsica\isAlphaNum; +use function Parsica\Parsica\isEqual; +use function Parsica\Parsica\orPred; +use function Parsica\Parsica\skipSpace; +use function Parsica\Parsica\string; +use function Parsica\Parsica\takeWhile; + +final class TagParser +{ + private static ?Parser $instance = null; + + public static function get(): Parser + { + return self::$instance ??= self::build(); + } + private static function build(): Parser + { + return char('<')->bind(fn () => + self::tagName()->bind(fn (string $tagName) => + skipSpace()->followedBy(AttributeParser::get())->thenIgnore(skipSpace())->bind(fn ($attributeNodes) => + either( + string('/>') + ->map(fn () => new TagNode($tagName, $attributeNodes, new TagContentNodes(), true)), + char('>')->followedBy(TagContentParser::get())->thenIgnore(self::tagClosing($tagName)) + ->map(fn ($tagContents) => new TagNode($tagName, $attributeNodes, $tagContents, false)) + ) + ) + ) + ); + } + + private static function tagName(): Parser + { + // @todo specification + return alphaChar()->append(takeWhile(orPred(isAlphaNum(), isEqual('-')))); + } + + private static function tagClosing(string $tagName): Parser + { + return assemble( + string('') + ); + } +} diff --git a/src/Parser/Parser/TagContent/TagContentParser.php b/src/Parser/Parser/TagContent/TagContentParser.php new file mode 100644 index 00000000..5637338e --- /dev/null +++ b/src/Parser/Parser/TagContent/TagContentParser.php @@ -0,0 +1,60 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Parser\Parser\TagContent; + +use PackageFactory\ComponentEngine\Parser\Ast\TagContentNode; +use PackageFactory\ComponentEngine\Parser\Ast\TagContentNodes; +use PackageFactory\ComponentEngine\Parser\Parser\Expression\ExpressionParser; +use PackageFactory\ComponentEngine\Parser\Parser\Tag\TagParser; +use PackageFactory\ComponentEngine\Parser\Parser\Text\TextParser; +use Parsica\Parsica\Parser; + +use function Parsica\Parsica\any; +use function Parsica\Parsica\char; +use function Parsica\Parsica\zeroOrMore; + +final class TagContentParser +{ + private static ?Parser $instance = null; + + public static function get(): Parser + { + return self::$instance ??= self::build(); + } + + private static function build(): Parser + { + return zeroOrMore( + self::tagContent() + )->map(fn ($collected) => new TagContentNodes(...$collected ? $collected : [])); + } + + private static function tagContent(): Parser + { + return any( + TagParser::get(), + TextParser::get(), + char('{')->followedBy(ExpressionParser::get())->thenIgnore(char('}')), + )->map(fn ($item) => [new TagContentNode($item)]); + } +} diff --git a/src/Parser/Parser/Text/TextParser.php b/src/Parser/Parser/Text/TextParser.php new file mode 100644 index 00000000..e0e5f2c5 --- /dev/null +++ b/src/Parser/Parser/Text/TextParser.php @@ -0,0 +1,44 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Parser\Parser\Text; + +use PackageFactory\ComponentEngine\Parser\Ast\TextNode; +use Parsica\Parsica\Parser; + +use function Parsica\Parsica\takeWhile1; + +final class TextParser +{ + private static ?Parser $instance = null; + + public static function get(): Parser + { + return self::$instance ??= self::build(); + } + + private static function build(): Parser + { + // @todo space handling and valid chars + return takeWhile1(fn ($char) => $char !== '<' && $char !== '{')->map(fn ($text) => new TextNode($text)); + } +} From fc5fbf7e713280b651941f169bde26d4a91a5251 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Fri, 19 May 2023 15:04:45 +0200 Subject: [PATCH 08/20] WIP: Parsica Improve Precedence handling and extract parser into separate utility --- src/Definition/Precedence.php | 98 ------------------- .../Parser/Expression/ExpressionParser.php | 23 +++-- src/Parser/Parser/PrecedenceParser.php | 94 ++++++++++++++++++ src/Parser/Parser/UtilityParser.php | 69 +++++++++++++ 4 files changed, 174 insertions(+), 110 deletions(-) create mode 100644 src/Parser/Parser/PrecedenceParser.php create mode 100644 src/Parser/Parser/UtilityParser.php diff --git a/src/Definition/Precedence.php b/src/Definition/Precedence.php index ccc31e95..5051649b 100644 --- a/src/Definition/Precedence.php +++ b/src/Definition/Precedence.php @@ -23,18 +23,6 @@ namespace PackageFactory\ComponentEngine\Definition; use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; -use Parsica\Parsica\Internal\Fail; -use Parsica\Parsica\Internal\Succeed; -use Parsica\Parsica\Parser; -use Parsica\Parsica\ParseResult; -use Parsica\Parsica\Stream; - -use function Parsica\Parsica\either; -use function Parsica\Parsica\fail; -use function Parsica\Parsica\lookAhead; -use function Parsica\Parsica\oneOf; -use function Parsica\Parsica\pure; -use function Parsica\Parsica\succeed; enum Precedence: int { @@ -53,19 +41,6 @@ enum Precedence: int case TERNARY = 3; case SEQUENCE = 1; - private const ARRAY_OF_STRINGS_TO_PRECEDENCE = [ - [['(', ')', '[', ']', '?.', '.'], self::ACCESS], - [['!'], self::UNARY], - [['*', '/', '%'], self::POINT], - [['+', '-'], self::DASH], - [['+', '-'], self::DASH], - [['>', '>=', '<', '<='], self::COMPARISON], - [['===', '!=='], self::EQUALITY], - [['&&'], self::LOGICAL_AND], - [['||'], self::LOGICAL_OR], - [['?', ':'], self::TERNARY] - ]; - public static function forTokenType(TokenType $tokenType): self { return match ($tokenType) { @@ -104,79 +79,6 @@ public static function forTokenType(TokenType $tokenType): self }; } - /** - * A multi character compatible version (eg. for strings) of {@see oneOf()} - * - * While one could leverage multiple string parsers, it's not really performance efficient: - * - * any(string('f'), string('bar')) - * - * the above can be rewritten like: - * - * strings(['f', 'bar']) - * - * @param array $strings - * @return Parser - */ - private static function strings(array $strings): Parser - { - $longestString = 0; - foreach ($strings as $string) { - $len = mb_strlen($string); - if ($longestString < $len) { - $longestString = $len; - } - } - return Parser::make('strings', function (Stream $stream) use($strings, $longestString): ParseResult { - if ($stream->isEOF()) { - return new Fail('strings', $stream); - } - $result = $stream->takeN($longestString); - foreach ($strings as $string) { - if (str_starts_with($result->chunk(), $string)) { - return new Succeed($string, $stream->takeN(mb_strlen($string))->stream()); - } - } - return new Fail('strings', $stream); - }); - } - - private static function getPrecedenceMatcher(): Parser - { - static $matcher; - if ($matcher) { - return $matcher; - } - $allStrings = []; - $stringToPrecedence = []; - foreach (self::ARRAY_OF_STRINGS_TO_PRECEDENCE as [$strings, $precedence]) { - foreach ($strings as $string) { - $allStrings[] = $string; - $stringToPrecedence[$string] = $precedence; - } - } - $matcher = - either( - lookAhead( - self::strings($allStrings)->map(function ($match) use($stringToPrecedence) { - return $stringToPrecedence[$match]; - }) - ), - pure(self::SEQUENCE) - ); - return $matcher; - } - - public function check(): Parser - { - return self::getPrecedenceMatcher()->bind(function (Precedence $precedence) { - if ($this->mustStopAt($precedence)) { - return fail(''); - } - return succeed(); - })->label('check Precedence(' . $this->name . ')'); - } - public function mustStopAt(self $precedence): bool { return $precedence->value <= $this->value; diff --git a/src/Parser/Parser/Expression/ExpressionParser.php b/src/Parser/Parser/Expression/ExpressionParser.php index 3b08d5cd..007bff4e 100644 --- a/src/Parser/Parser/Expression/ExpressionParser.php +++ b/src/Parser/Parser/Expression/ExpressionParser.php @@ -31,6 +31,7 @@ use PackageFactory\ComponentEngine\Parser\Parser\Match\MatchParser; use PackageFactory\ComponentEngine\Parser\Parser\NullLiteral\NullLiteralParser; use PackageFactory\ComponentEngine\Parser\Parser\NumberLiteral\NumberLiteralParser; +use PackageFactory\ComponentEngine\Parser\Parser\PrecedenceParser; use PackageFactory\ComponentEngine\Parser\Parser\StringLiteral\StringLiteralParser; use PackageFactory\ComponentEngine\Parser\Parser\Tag\TagParser; use PackageFactory\ComponentEngine\Parser\Parser\TernaryOperation\TernaryOperationParser; @@ -41,6 +42,7 @@ final class ExpressionParser { + /** @return Parser */ public static function get(Precedence $precedence = Precedence::SEQUENCE): Parser { $expressionRootParser = between(skipSpace(), skipSpace(), any( @@ -55,29 +57,26 @@ public static function get(Precedence $precedence = Precedence::SEQUENCE): Parse )); return $expressionRootParser->map(fn ($expressionRoot) => new ExpressionNode($expressionRoot)) - ->bind(function ($expressionNode) use ($precedence) { - return self::continueParsing($expressionNode, $precedence); - }); + ->bind(fn ($expressionNode) => self::continueParsingWhilePrecedence($expressionNode, $precedence) + ); } - private static function continueParsing(ExpressionNode $expressionNode, Precedence $precedence): Parser + /** @return Parser */ + private static function continueParsingWhilePrecedence(ExpressionNode $expressionNode, Precedence $precedence): Parser { - $continuationParsers = any( + $continuationParser = any( BinaryOperationParser::get($expressionNode), AccessParser::get($expressionNode), TernaryOperationParser::get($expressionNode) ) ->thenIgnore(skipSpace()) - ->bind(function ($expressionRoot) use ($precedence) { - $newExpressionNode = new ExpressionNode($expressionRoot); - return self::continueParsing($newExpressionNode, $precedence); - }); + ->bind( + fn ($expressionRoot) => self::continueParsingWhilePrecedence(new ExpressionNode($expressionRoot), $precedence) + ); return either( - $precedence->check() - ->sequence($continuationParsers), + PrecedenceParser::hasPrecedence($precedence)->sequence($continuationParser), pure($expressionNode) ); } } - diff --git a/src/Parser/Parser/PrecedenceParser.php b/src/Parser/Parser/PrecedenceParser.php new file mode 100644 index 00000000..12111528 --- /dev/null +++ b/src/Parser/Parser/PrecedenceParser.php @@ -0,0 +1,94 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Parser\Parser; + +use PackageFactory\ComponentEngine\Definition\Precedence; +use Parsica\Parsica\Parser; + +use function Parsica\Parsica\either; +use function Parsica\Parsica\fail; +use function Parsica\Parsica\lookAhead; +use function Parsica\Parsica\pure; +use function Parsica\Parsica\succeed; + +final class PrecedenceParser +{ + /** + * @var list, 1: Precedence}> + */ + private const STRINGS_TO_PRECEDENCE = [ + [['(', ')', '[', ']', '?.', '.'], Precedence::ACCESS], + [['!'], Precedence::UNARY], + [['*', '/', '%'], Precedence::POINT], + [['+', '-'], Precedence::DASH], + [['+', '-'], Precedence::DASH], + [['>', '>=', '<', '<='], Precedence::COMPARISON], + [['===', '!=='], Precedence::EQUALITY], + [['&&'], Precedence::LOGICAL_AND], + [['||'], Precedence::LOGICAL_OR], + [['?', ':'], Precedence::TERNARY] + ]; + + private static ?Parser $precedenceLookahead = null; + + /** + * Look ahead to see if the precedence from the next characters is less than the given + * + * @return Parser + */ + public static function hasPrecedence(Precedence $precedence): Parser + { + return self::precedenceLookahead()->bind(function (Precedence $precedenceByNextCharacters) use ($precedence) { + if ($precedence->mustStopAt($precedenceByNextCharacters)) { + return fail($precedence->name . ' must stop at ' . $precedenceByNextCharacters->name); + } + return succeed(); + })->label('precedence(' . $precedence->name . ')'); + } + + /** + * @return Parser + */ + private static function precedenceLookahead(): Parser + { + if (self::$precedenceLookahead) { + return self::$precedenceLookahead; + } + $allStrings = []; + $stringToPrecedence = []; + foreach (self::STRINGS_TO_PRECEDENCE as [$strings, $precedence]) { + foreach ($strings as $string) { + $allStrings[] = $string; + $stringToPrecedence[$string] = $precedence; + } + } + return self::$precedenceLookahead = either( + lookAhead( + UtilityParser::strings($allStrings)->map(function ($match) use($stringToPrecedence) { + return $stringToPrecedence[$match]; + }) + ), + pure(Precedence::SEQUENCE) + ); + } +} diff --git a/src/Parser/Parser/UtilityParser.php b/src/Parser/Parser/UtilityParser.php new file mode 100644 index 00000000..70fe3fdd --- /dev/null +++ b/src/Parser/Parser/UtilityParser.php @@ -0,0 +1,69 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Parser\Parser; + +use Parsica\Parsica\Internal\Fail; +use Parsica\Parsica\Internal\Succeed; +use Parsica\Parsica\Parser; +use Parsica\Parsica\ParseResult; +use Parsica\Parsica\Stream; + +final class UtilityParser +{ + /** + * A multi character compatible version (eg. for strings) of {@see oneOf()} + * + * While one could leverage multiple string parsers, it's not really performance efficient: + * + * any(string('f'), string('bar')) + * + * the above can be rewritten like: + * + * strings(['f', 'bar']) + * + * @param array $strings + * @return Parser + */ + public static function strings(array $strings): Parser + { + $longestString = 0; + foreach ($strings as $string) { + $len = mb_strlen($string); + if ($longestString < $len) { + $longestString = $len; + } + } + return Parser::make('strings', function (Stream $stream) use($strings, $longestString): ParseResult { + if ($stream->isEOF()) { + return new Fail('strings', $stream); + } + $result = $stream->takeN($longestString); + foreach ($strings as $string) { + if (str_starts_with($result->chunk(), $string)) { + return new Succeed($string, $stream->takeN(mb_strlen($string))->stream()); + } + } + return new Fail('strings', $stream); + }); + } +} From 3870cd6da41b9402b34fd030a4311c0973da72d0 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Fri, 19 May 2023 15:31:36 +0200 Subject: [PATCH 09/20] WIP: Parsica parse TemplateLiteralNode All ExpressionParser tests are green --- src/Parser/Ast/TemplateLiteralNode.php | 2 +- .../Parser/Expression/ExpressionParser.php | 2 + .../StringLiteral/StringLiteralParser.php | 31 +-------- .../TemplateLiteral/TemplateLiteralParser.php | 68 +++++++++++++++++++ src/Parser/Parser/UtilityParser.php | 28 ++++++++ 5 files changed, 101 insertions(+), 30 deletions(-) create mode 100644 src/Parser/Parser/TemplateLiteral/TemplateLiteralParser.php diff --git a/src/Parser/Ast/TemplateLiteralNode.php b/src/Parser/Ast/TemplateLiteralNode.php index 83d38c11..bad3a7a5 100644 --- a/src/Parser/Ast/TemplateLiteralNode.php +++ b/src/Parser/Ast/TemplateLiteralNode.php @@ -35,7 +35,7 @@ final class TemplateLiteralNode implements \JsonSerializable */ public readonly array $segments; - private function __construct( + public function __construct( StringLiteralNode|ExpressionNode ...$segments ) { $this->segments = $segments; diff --git a/src/Parser/Parser/Expression/ExpressionParser.php b/src/Parser/Parser/Expression/ExpressionParser.php index 007bff4e..851e3ce4 100644 --- a/src/Parser/Parser/Expression/ExpressionParser.php +++ b/src/Parser/Parser/Expression/ExpressionParser.php @@ -34,6 +34,7 @@ use PackageFactory\ComponentEngine\Parser\Parser\PrecedenceParser; use PackageFactory\ComponentEngine\Parser\Parser\StringLiteral\StringLiteralParser; use PackageFactory\ComponentEngine\Parser\Parser\Tag\TagParser; +use PackageFactory\ComponentEngine\Parser\Parser\TemplateLiteral\TemplateLiteralParser; use PackageFactory\ComponentEngine\Parser\Parser\TernaryOperation\TernaryOperationParser; use PackageFactory\ComponentEngine\Parser\Parser\UnaryOperation\UnaryOperationParser; use Parsica\Parsica\Parser; @@ -53,6 +54,7 @@ public static function get(Precedence $precedence = Precedence::SEQUENCE): Parse TagParser::get(), StringLiteralParser::get(), IdentifierParser::get(), + TemplateLiteralParser::get(), UnaryOperationParser::get() )); diff --git a/src/Parser/Parser/StringLiteral/StringLiteralParser.php b/src/Parser/Parser/StringLiteral/StringLiteralParser.php index 0de48890..ad954b1a 100644 --- a/src/Parser/Parser/StringLiteral/StringLiteralParser.php +++ b/src/Parser/Parser/StringLiteral/StringLiteralParser.php @@ -23,10 +23,9 @@ namespace PackageFactory\ComponentEngine\Parser\Parser\StringLiteral; use PackageFactory\ComponentEngine\Parser\Ast\StringLiteralNode; +use PackageFactory\ComponentEngine\Parser\Parser\UtilityParser; use Parsica\Parsica\Parser; -use function Parsica\Parsica\{anySingle, append, between, char, assemble, either, takeWhile, zeroOrMore}; - final class StringLiteralParser { private static ?Parser $instance = null; @@ -38,32 +37,6 @@ public static function get(): Parser private static function build(): Parser { - // labels, and escaping handling - // also maybe use bind with the start character instead of two parsers - return either( - self::forQuotedStringType('\''), - self::forQuotedStringType('"') - )->map(fn (string $contents) => new StringLiteralNode($contents)); - } - - private static function forQuotedStringType(string $qouteType): Parser - { - assert($qouteType === '"' || $qouteType === '\''); - $takeAllNonBackslashesAndQuoteChars = takeWhile( - fn (string $char): bool => $char !== $qouteType && $char !== '\\' - ); - return between( - char($qouteType), - char($qouteType), - append( - $takeAllNonBackslashesAndQuoteChars, - zeroOrMore( - append( - char("\\")->followedBy(anySingle()), - $takeAllNonBackslashesAndQuoteChars, - ) - ) - ) - ); + return UtilityParser::quotedStringContents()->map(fn (string $contents) => new StringLiteralNode($contents)); } } diff --git a/src/Parser/Parser/TemplateLiteral/TemplateLiteralParser.php b/src/Parser/Parser/TemplateLiteral/TemplateLiteralParser.php new file mode 100644 index 00000000..709c354d --- /dev/null +++ b/src/Parser/Parser/TemplateLiteral/TemplateLiteralParser.php @@ -0,0 +1,68 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Parser\Parser\TemplateLiteral; + +use PackageFactory\ComponentEngine\Parser\Ast\StringLiteralNode; +use PackageFactory\ComponentEngine\Parser\Ast\TemplateLiteralNode; +use PackageFactory\ComponentEngine\Parser\Parser\Expression\ExpressionParser; +use Parsica\Parsica\Parser; + +use function Parsica\Parsica\any; +use function Parsica\Parsica\char; +use function Parsica\Parsica\string; +use function Parsica\Parsica\takeWhile1; +use function Parsica\Parsica\zeroOrMore; + +final class TemplateLiteralParser +{ + private static ?Parser $instance = null; + + public static function get(): Parser + { + return self::$instance ??= self::build(); + } + + private static function build(): Parser + { + return char('`')->bind( + fn () => zeroOrMore( + any( + self::stringLiteral(), + self::expression(), + )->map(fn ($item) => [$item]) + )->map(fn ($collected) => new TemplateLiteralNode(...$collected ? $collected : [])) + ->thenIgnore(char('`')) + ); + } + + private static function expression(): Parser + { + return string('${')->followedBy(ExpressionParser::get())->thenIgnore(char('}')); + } + + private static function stringLiteral(): Parser + { + // @todo escapes? or allow `single unescaped $ dollar?` + return takeWhile1(fn ($char) => $char !== '$' && $char !== '`')->map(fn ($text) => new StringLiteralNode($text)); + } +} diff --git a/src/Parser/Parser/UtilityParser.php b/src/Parser/Parser/UtilityParser.php index 70fe3fdd..13877661 100644 --- a/src/Parser/Parser/UtilityParser.php +++ b/src/Parser/Parser/UtilityParser.php @@ -28,8 +28,36 @@ use Parsica\Parsica\ParseResult; use Parsica\Parsica\Stream; +use function Parsica\Parsica\anySingle; +use function Parsica\Parsica\append; +use function Parsica\Parsica\char; +use function Parsica\Parsica\oneOfS; +use function Parsica\Parsica\takeWhile; +use function Parsica\Parsica\zeroOrMore; + final class UtilityParser { + /** + * @return Parser + */ + public static function quotedStringContents(): Parser + { + // labels, and escaping handling + return oneOfS('"\'')->bind( + fn (string $startingQuoteChar) => append( + $simpleCase = takeWhile( + fn (string $char): bool => $char !== $startingQuoteChar && $char !== '\\' + ), + zeroOrMore( + append( + char('\\')->followedBy(anySingle()), + $simpleCase, + ) + ) + )->thenIgnore(char($startingQuoteChar)) + ); + } + /** * A multi character compatible version (eg. for strings) of {@see oneOf()} * From ab5ddd3ddb27cbbec24d3b267dc4be93ffdba6a2 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Fri, 19 May 2023 16:01:23 +0200 Subject: [PATCH 10/20] WIP: Parsica cleanup MatchParser --- src/Parser/Parser/Match/MatchParser.php | 54 ++++----------- src/Parser/Parser/MatchArm/MatchArmParser.php | 68 +++++++++++++++++++ 2 files changed, 82 insertions(+), 40 deletions(-) create mode 100644 src/Parser/Parser/MatchArm/MatchArmParser.php diff --git a/src/Parser/Parser/Match/MatchParser.php b/src/Parser/Parser/Match/MatchParser.php index 7856ab79..1ec2dbf9 100644 --- a/src/Parser/Parser/Match/MatchParser.php +++ b/src/Parser/Parser/Match/MatchParser.php @@ -22,20 +22,15 @@ namespace PackageFactory\ComponentEngine\Parser\Parser\Match; -use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNode; -use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNodes; -use PackageFactory\ComponentEngine\Parser\Ast\MatchArmNode; -use PackageFactory\ComponentEngine\Parser\Ast\MatchArmNodes; use PackageFactory\ComponentEngine\Parser\Ast\MatchNode; use PackageFactory\ComponentEngine\Parser\Parser\Expression\ExpressionParser; +use PackageFactory\ComponentEngine\Parser\Parser\MatchArm\MatchArmParser; use Parsica\Parsica\Parser; use function Parsica\Parsica\char; use function Parsica\Parsica\collect; -use function Parsica\Parsica\either; use function Parsica\Parsica\skipSpace; use function Parsica\Parsica\string; -use function Parsica\Parsica\zeroOrMore; final class MatchParser { @@ -50,39 +45,18 @@ private static function build(): Parser { // @todo for some reason we must use bind here to avoid infinite recursion return string('match') - ->bind(fn () => skipSpace() - ->followedBy(char('(')) - ->followedBy(ExpressionParser::get()) - ->thenIgnore(char(')')) - ->thenIgnore(skipSpace()) - ->thenIgnore(char('{')) - ->thenIgnore(skipSpace()) - ->bind(fn ($expression) => self::getMatchArmsParser()->map(fn ($matchArmNodes) => new MatchNode($expression, $matchArmNodes))) - ->thenIgnore(skipSpace()) - ->thenIgnore(char('}')) - ); - } - - private static function getMatchArmParser(): Parser - { - return collect( - either( - string('default')->map(fn () => null), - ExpressionParser::get()->map(fn (ExpressionNode $expressionNode) => new ExpressionNodes($expressionNode)) - ), - skipSpace(), - string('->'), - skipSpace(), - ExpressionParser::get(), - skipSpace(), - )->map(fn ($collected) => new MatchArmNode($collected[0], $collected[4])); - - } - - private static function getMatchArmsParser(): Parser - { - return zeroOrMore( - self::getMatchArmParser()->map(fn ($matchArmNode) => [$matchArmNode]) - )->map(fn ($matchArmNodes) => new MatchArmNodes(...$matchArmNodes ? $matchArmNodes : [])); + ->bind(fn () => collect( + skipSpace(), + char('('), + ExpressionParser::get(), + char(')'), + skipSpace(), + char('{'), + skipSpace(), + MatchArmParser::get(), + skipSpace(), + char('}') + )->map(fn ($collected) => new MatchNode($collected[2], $collected[7])) + ); } } diff --git a/src/Parser/Parser/MatchArm/MatchArmParser.php b/src/Parser/Parser/MatchArm/MatchArmParser.php new file mode 100644 index 00000000..7c9c8e5c --- /dev/null +++ b/src/Parser/Parser/MatchArm/MatchArmParser.php @@ -0,0 +1,68 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Parser\Parser\MatchArm; + +use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNode; +use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNodes; +use PackageFactory\ComponentEngine\Parser\Ast\MatchArmNode; +use PackageFactory\ComponentEngine\Parser\Ast\MatchArmNodes; +use PackageFactory\ComponentEngine\Parser\Parser\Expression\ExpressionParser; +use Parsica\Parsica\Parser; + +use function Parsica\Parsica\collect; +use function Parsica\Parsica\either; +use function Parsica\Parsica\skipSpace; +use function Parsica\Parsica\string; +use function Parsica\Parsica\zeroOrMore; + +final class MatchArmParser +{ + private static ?Parser $instance = null; + + public static function get(): Parser + { + return self::$instance ??= self::build(); + } + + private static function build(): Parser + { + return zeroOrMore( + self::getMatchArmParser()->map(fn ($matchArmNode) => [$matchArmNode]) + )->map(fn ($matchArmNodes) => new MatchArmNodes(...$matchArmNodes ? $matchArmNodes : [])); + } + + private static function getMatchArmParser(): Parser + { + return collect( + either( + string('default')->map(fn () => null), + ExpressionParser::get()->map(fn (ExpressionNode $expressionNode) => new ExpressionNodes($expressionNode)) + ), + skipSpace(), + string('->'), + skipSpace(), + ExpressionParser::get(), + skipSpace(), + )->map(fn ($collected) => new MatchArmNode($collected[0], $collected[4])); + } +} From df231cdcc760b752f84f8453389383786f37413b Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Fri, 19 May 2023 17:58:04 +0200 Subject: [PATCH 11/20] WIP: Parsica minor cleanups --- src/Parser/Ast/ExpressionNode.php | 3 ++- src/Parser/Parser/Access/AccessParser.php | 6 +++--- src/Parser/Parser/Expression/ExpressionParser.php | 9 +++++++-- src/Parser/Parser/Identifier/IdentifierParser.php | 3 ++- src/Parser/Parser/MatchArm/MatchArmParser.php | 6 +++--- src/Parser/Parser/TagContent/TagContentParser.php | 6 +++--- src/Parser/Parser/UtilityParser.php | 13 +++++++++++++ 7 files changed, 33 insertions(+), 13 deletions(-) diff --git a/src/Parser/Ast/ExpressionNode.php b/src/Parser/Ast/ExpressionNode.php index 922f6255..b250ab04 100644 --- a/src/Parser/Ast/ExpressionNode.php +++ b/src/Parser/Ast/ExpressionNode.php @@ -36,9 +36,10 @@ public function __construct( ) { } + /** @deprecated */ public static function fromString(string $expressionAsString): self { - return ExpressionParser::get()->tryString($expressionAsString)->output(); + return ExpressionParser::parseFromString($expressionAsString); } /** diff --git a/src/Parser/Parser/Access/AccessParser.php b/src/Parser/Parser/Access/AccessParser.php index e18698fd..ed7f4a8e 100644 --- a/src/Parser/Parser/Access/AccessParser.php +++ b/src/Parser/Parser/Access/AccessParser.php @@ -30,21 +30,21 @@ use PackageFactory\ComponentEngine\Parser\Parser\Identifier\IdentifierParser; use Parsica\Parsica\Parser; -use function Parsica\Parsica\atLeastOne; use function Parsica\Parsica\char; use function Parsica\Parsica\collect; use function Parsica\Parsica\either; +use function Parsica\Parsica\some; use function Parsica\Parsica\string; final class AccessParser { public static function get(ExpressionNode $subject): Parser { - return atLeastOne( + return some( collect( self::accessType(), IdentifierParser::get() - )->map(fn ($result) => [new AccessChainSegmentNode($result[0], $result[1])]) + )->map(fn ($result) => new AccessChainSegmentNode($result[0], $result[1])) )->map(fn ($segments) => new AccessNode( $subject, new AccessChainSegmentNodes(...$segments) diff --git a/src/Parser/Parser/Expression/ExpressionParser.php b/src/Parser/Parser/Expression/ExpressionParser.php index 851e3ce4..bff72985 100644 --- a/src/Parser/Parser/Expression/ExpressionParser.php +++ b/src/Parser/Parser/Expression/ExpressionParser.php @@ -43,6 +43,11 @@ final class ExpressionParser { + public static function parseFromString(string $expressionAsString): ExpressionNode + { + return self::get()->thenEof()->tryString($expressionAsString)->output(); + } + /** @return Parser */ public static function get(Precedence $precedence = Precedence::SEQUENCE): Parser { @@ -58,8 +63,8 @@ public static function get(Precedence $precedence = Precedence::SEQUENCE): Parse UnaryOperationParser::get() )); - return $expressionRootParser->map(fn ($expressionRoot) => new ExpressionNode($expressionRoot)) - ->bind(fn ($expressionNode) => self::continueParsingWhilePrecedence($expressionNode, $precedence) + return $expressionRootParser->bind( + fn ($expressionRoot) => self::continueParsingWhilePrecedence(new ExpressionNode($expressionRoot), $precedence) ); } diff --git a/src/Parser/Parser/Identifier/IdentifierParser.php b/src/Parser/Parser/Identifier/IdentifierParser.php index 6398212e..72f6b658 100644 --- a/src/Parser/Parser/Identifier/IdentifierParser.php +++ b/src/Parser/Parser/Identifier/IdentifierParser.php @@ -23,6 +23,7 @@ namespace PackageFactory\ComponentEngine\Parser\Parser\Identifier; use PackageFactory\ComponentEngine\Parser\Ast\IdentifierNode; +use PackageFactory\ComponentEngine\Parser\Parser\UtilityParser; use Parsica\Parsica\Parser; use function Parsica\Parsica\{alphaChar, alphaNumChar, zeroOrMore}; @@ -38,7 +39,7 @@ public static function get(): Parser private static function build(): Parser { - return alphaChar()->append(zeroOrMore(alphaNumChar())) + return UtilityParser::identifier() ->map(fn ($name) => new IdentifierNode($name)); } } diff --git a/src/Parser/Parser/MatchArm/MatchArmParser.php b/src/Parser/Parser/MatchArm/MatchArmParser.php index 7c9c8e5c..526d2a01 100644 --- a/src/Parser/Parser/MatchArm/MatchArmParser.php +++ b/src/Parser/Parser/MatchArm/MatchArmParser.php @@ -31,9 +31,9 @@ use function Parsica\Parsica\collect; use function Parsica\Parsica\either; +use function Parsica\Parsica\many; use function Parsica\Parsica\skipSpace; use function Parsica\Parsica\string; -use function Parsica\Parsica\zeroOrMore; final class MatchArmParser { @@ -46,8 +46,8 @@ public static function get(): Parser private static function build(): Parser { - return zeroOrMore( - self::getMatchArmParser()->map(fn ($matchArmNode) => [$matchArmNode]) + return many( + self::getMatchArmParser() )->map(fn ($matchArmNodes) => new MatchArmNodes(...$matchArmNodes ? $matchArmNodes : [])); } diff --git a/src/Parser/Parser/TagContent/TagContentParser.php b/src/Parser/Parser/TagContent/TagContentParser.php index 5637338e..2dd75044 100644 --- a/src/Parser/Parser/TagContent/TagContentParser.php +++ b/src/Parser/Parser/TagContent/TagContentParser.php @@ -31,7 +31,7 @@ use function Parsica\Parsica\any; use function Parsica\Parsica\char; -use function Parsica\Parsica\zeroOrMore; +use function Parsica\Parsica\many; final class TagContentParser { @@ -44,7 +44,7 @@ public static function get(): Parser private static function build(): Parser { - return zeroOrMore( + return many( self::tagContent() )->map(fn ($collected) => new TagContentNodes(...$collected ? $collected : [])); } @@ -55,6 +55,6 @@ private static function tagContent(): Parser TagParser::get(), TextParser::get(), char('{')->followedBy(ExpressionParser::get())->thenIgnore(char('}')), - )->map(fn ($item) => [new TagContentNode($item)]); + )->map(fn ($item) => new TagContentNode($item)); } } diff --git a/src/Parser/Parser/UtilityParser.php b/src/Parser/Parser/UtilityParser.php index 13877661..f28e3d6c 100644 --- a/src/Parser/Parser/UtilityParser.php +++ b/src/Parser/Parser/UtilityParser.php @@ -28,15 +28,28 @@ use Parsica\Parsica\ParseResult; use Parsica\Parsica\Stream; +use function Parsica\Parsica\alphaChar; +use function Parsica\Parsica\alphaNumChar; use function Parsica\Parsica\anySingle; use function Parsica\Parsica\append; use function Parsica\Parsica\char; use function Parsica\Parsica\oneOfS; +use function Parsica\Parsica\string; use function Parsica\Parsica\takeWhile; use function Parsica\Parsica\zeroOrMore; final class UtilityParser { + public static function identifier(): Parser + { + return alphaChar()->append(zeroOrMore(alphaNumChar())); + } + + public static function keyword(string $keyword): Parser + { + return string($keyword)->notFollowedBy(alphaNumChar()); + } + /** * @return Parser */ From 018c156c409a0b0ad48a66f694ad280b758f327b Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Fri, 19 May 2023 21:25:15 +0200 Subject: [PATCH 12/20] WIP: Parsica implement Module Import Export Component Enum and Struct Test pass :D --- .../Loader/ModuleFile/ModuleFileLoader.php | 2 +- src/Parser/Ast/AttributeNode.php | 2 +- src/Parser/Ast/ComponentDeclarationNode.php | 11 +-- src/Parser/Ast/EnumDeclarationNode.php | 9 +-- src/Parser/Ast/EnumMemberDeclarationNode.php | 2 +- src/Parser/Ast/EnumMemberDeclarationNodes.php | 2 +- src/Parser/Ast/ExportNode.php | 2 +- src/Parser/Ast/ExportNodes.php | 30 ++++--- src/Parser/Ast/ImportNode.php | 7 +- src/Parser/Ast/ImportNodes.php | 26 +++++-- src/Parser/Ast/ModuleNode.php | 9 +-- src/Parser/Ast/PropertyDeclarationNode.php | 2 +- src/Parser/Ast/PropertyDeclarationNodes.php | 21 +++-- src/Parser/Ast/StructDeclarationNode.php | 9 +-- src/Parser/Ast/TypeReferenceNode.php | 2 +- .../ComponentDeclarationParser.php | 78 +++++++++++++++++++ .../EnumDeclaration/EnumDeclarationParser.php | 66 ++++++++++++++++ .../EnumMemberDeclarationParser.php | 74 ++++++++++++++++++ src/Parser/Parser/Export/ExportParser.php | 62 +++++++++++++++ .../Parser/Expression/ExpressionParser.php | 4 +- src/Parser/Parser/Import/ImportParser.php | 73 +++++++++++++++++ src/Parser/Parser/Module/ModuleParser.php | 75 ++++++++++++++++++ .../PropertyDeclarationParser.php | 58 ++++++++++++++ .../StructDeclarationParser.php | 66 ++++++++++++++++ .../TypeReference/TypeReferenceParser.php | 57 ++++++++++++++ 25 files changed, 683 insertions(+), 66 deletions(-) create mode 100644 src/Parser/Parser/ComponentDeclaration/ComponentDeclarationParser.php create mode 100644 src/Parser/Parser/EnumDeclaration/EnumDeclarationParser.php create mode 100644 src/Parser/Parser/EnumMemberDeclaration/EnumMemberDeclarationParser.php create mode 100644 src/Parser/Parser/Export/ExportParser.php create mode 100644 src/Parser/Parser/Import/ImportParser.php create mode 100644 src/Parser/Parser/Module/ModuleParser.php create mode 100644 src/Parser/Parser/PropertyDeclaration/PropertyDeclarationParser.php create mode 100644 src/Parser/Parser/StructDeclaration/StructDeclarationParser.php create mode 100644 src/Parser/Parser/TypeReference/TypeReferenceParser.php diff --git a/src/Module/Loader/ModuleFile/ModuleFileLoader.php b/src/Module/Loader/ModuleFile/ModuleFileLoader.php index eaf342f8..e4719132 100644 --- a/src/Module/Loader/ModuleFile/ModuleFileLoader.php +++ b/src/Module/Loader/ModuleFile/ModuleFileLoader.php @@ -40,7 +40,7 @@ final class ModuleFileLoader implements LoaderInterface { public function resolveTypeOfImport(ImportNode $importNode): TypeInterface { - $pathToImportFrom = $importNode->source->path->resolveRelationTo( + $pathToImportFrom = $importNode->sourcePath->resolveRelationTo( Path::fromString($importNode->path) ); $source = Source::fromFile($pathToImportFrom->value); diff --git a/src/Parser/Ast/AttributeNode.php b/src/Parser/Ast/AttributeNode.php index 7c140eb1..a898aa1e 100644 --- a/src/Parser/Ast/AttributeNode.php +++ b/src/Parser/Ast/AttributeNode.php @@ -30,7 +30,7 @@ final class AttributeNode implements \JsonSerializable { - private function __construct( + public function __construct( public readonly string $name, public readonly ExpressionNode | StringLiteralNode $value ) { diff --git a/src/Parser/Ast/ComponentDeclarationNode.php b/src/Parser/Ast/ComponentDeclarationNode.php index f5f8c8e4..0823791f 100644 --- a/src/Parser/Ast/ComponentDeclarationNode.php +++ b/src/Parser/Ast/ComponentDeclarationNode.php @@ -22,15 +22,14 @@ namespace PackageFactory\ComponentEngine\Parser\Ast; -use PackageFactory\ComponentEngine\Parser\Source\Source; +use PackageFactory\ComponentEngine\Parser\Parser\ComponentDeclaration\ComponentDeclarationParser; use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Tokenizer; use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; final class ComponentDeclarationNode implements \JsonSerializable { - private function __construct( + public function __construct( public readonly string $componentName, public readonly PropertyDeclarationNodes $propertyDeclarations, public readonly ExpressionNode $returnExpression @@ -39,11 +38,7 @@ private function __construct( public static function fromString(string $componentDeclarationAsString): self { - return self::fromTokens( - Tokenizer::fromSource( - Source::fromString($componentDeclarationAsString) - )->getIterator() - ); + return ComponentDeclarationParser::parseFromString($componentDeclarationAsString); } /** diff --git a/src/Parser/Ast/EnumDeclarationNode.php b/src/Parser/Ast/EnumDeclarationNode.php index ba27a873..3195abb3 100644 --- a/src/Parser/Ast/EnumDeclarationNode.php +++ b/src/Parser/Ast/EnumDeclarationNode.php @@ -22,6 +22,7 @@ namespace PackageFactory\ComponentEngine\Parser\Ast; +use PackageFactory\ComponentEngine\Parser\Parser\EnumDeclaration\EnumDeclarationParser; use PackageFactory\ComponentEngine\Parser\Source\Source; use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; @@ -30,7 +31,7 @@ final class EnumDeclarationNode implements \JsonSerializable { - private function __construct( + public function __construct( public readonly string $enumName, public readonly EnumMemberDeclarationNodes $memberDeclarations ) { @@ -38,11 +39,7 @@ private function __construct( public static function fromString(string $enumDeclarationAsString): self { - return self::fromTokens( - Tokenizer::fromSource( - Source::fromString($enumDeclarationAsString) - )->getIterator() - ); + return EnumDeclarationParser::parseFromString($enumDeclarationAsString); } /** diff --git a/src/Parser/Ast/EnumMemberDeclarationNode.php b/src/Parser/Ast/EnumMemberDeclarationNode.php index 7b00a30e..53fdc8f3 100644 --- a/src/Parser/Ast/EnumMemberDeclarationNode.php +++ b/src/Parser/Ast/EnumMemberDeclarationNode.php @@ -28,7 +28,7 @@ final class EnumMemberDeclarationNode implements \JsonSerializable { - private function __construct( + public function __construct( public readonly string $name, public readonly null|StringLiteralNode|NumberLiteralNode $value ) { diff --git a/src/Parser/Ast/EnumMemberDeclarationNodes.php b/src/Parser/Ast/EnumMemberDeclarationNodes.php index ad00c190..95f3a05c 100644 --- a/src/Parser/Ast/EnumMemberDeclarationNodes.php +++ b/src/Parser/Ast/EnumMemberDeclarationNodes.php @@ -33,7 +33,7 @@ final class EnumMemberDeclarationNodes implements \JsonSerializable */ public readonly array $items; - private function __construct( + public function __construct( EnumMemberDeclarationNode ...$items ) { $itemsAsHashMap = []; diff --git a/src/Parser/Ast/ExportNode.php b/src/Parser/Ast/ExportNode.php index 0d060a00..fd75e43c 100644 --- a/src/Parser/Ast/ExportNode.php +++ b/src/Parser/Ast/ExportNode.php @@ -28,7 +28,7 @@ final class ExportNode implements \JsonSerializable { - private function __construct( + public function __construct( public readonly ComponentDeclarationNode | EnumDeclarationNode | StructDeclarationNode $declaration, ) { } diff --git a/src/Parser/Ast/ExportNodes.php b/src/Parser/Ast/ExportNodes.php index 8b8b90e7..e9c23d57 100644 --- a/src/Parser/Ast/ExportNodes.php +++ b/src/Parser/Ast/ExportNodes.php @@ -22,10 +22,6 @@ namespace PackageFactory\ComponentEngine\Parser\Ast; -use PackageFactory\ComponentEngine\Parser\Ast\ComponentDeclarationNode; -use PackageFactory\ComponentEngine\Parser\Ast\EnumDeclarationNode; -use PackageFactory\ComponentEngine\Parser\Ast\InterfaceDeclarationNode; - final class ExportNodes implements \JsonSerializable { /** @@ -33,18 +29,28 @@ final class ExportNodes implements \JsonSerializable */ public readonly array $items; - /** - * @param array $items - */ - private function __construct( - array $items + public function __construct( + ExportNode ...$items ) { - $this->items = $items; + $itemsAsHashMap = []; + foreach ($items as $item) { + $name = match ($item->declaration::class) { + ComponentDeclarationNode::class => $item->declaration->componentName, + StructDeclarationNode::class => $item->declaration->structName, + EnumDeclarationNode::class => $item->declaration->enumName + }; + if (array_key_exists($name, $itemsAsHashMap)) { + throw new \Exception('@TODO: Duplicate Export ' . $name); + } + $itemsAsHashMap[$name] = $item; + } + + $this->items = $itemsAsHashMap; } public static function empty(): self { - return new self([]); + return new self(); } public function withAddedExport(ExportNode $export): self @@ -59,7 +65,7 @@ public function withAddedExport(ExportNode $export): self throw new \Exception('@TODO: Duplicate Export ' . $name); } - return new self([...$this->items, ...[$name => $export]]); + return new self(...[...$this->items, ...[$name => $export]]); } public function get(string $name): ?ExportNode diff --git a/src/Parser/Ast/ImportNode.php b/src/Parser/Ast/ImportNode.php index 32446b37..b7886c8b 100644 --- a/src/Parser/Ast/ImportNode.php +++ b/src/Parser/Ast/ImportNode.php @@ -23,6 +23,7 @@ namespace PackageFactory\ComponentEngine\Parser\Ast; use PackageFactory\ComponentEngine\Parser\Ast\IdentifierNode; +use PackageFactory\ComponentEngine\Parser\Source\Path; use PackageFactory\ComponentEngine\Parser\Source\Source; use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; @@ -30,8 +31,8 @@ final class ImportNode implements \JsonSerializable { - private function __construct( - public readonly Source $source, + public function __construct( + public readonly Path $sourcePath, public readonly string $path, public readonly IdentifierNode $name ) { @@ -64,7 +65,7 @@ public static function fromTokens(\Iterator $tokens): \Iterator while (true) { $identifier = IdentifierNode::fromTokens($tokens); - yield new self($source, $path, $identifier); + yield new self($source->path, $path, $identifier); Scanner::skipSpaceAndComments($tokens); if (Scanner::type($tokens) === TokenType::COMMA) { diff --git a/src/Parser/Ast/ImportNodes.php b/src/Parser/Ast/ImportNodes.php index f898ff88..670bdd45 100644 --- a/src/Parser/Ast/ImportNodes.php +++ b/src/Parser/Ast/ImportNodes.php @@ -29,18 +29,28 @@ final class ImportNodes implements \JsonSerializable */ public readonly array $items; - /** - * @param array $items - */ - private function __construct( - array $items + public function __construct( + ImportNode ...$items ) { - $this->items = $items; + $itemsAsHashMap = []; + foreach ($items as $item) { + if (array_key_exists($item->name->value, $itemsAsHashMap)) { + throw new \Exception('@TODO: Duplicate Import ' . $item->name->value); + } + $itemsAsHashMap[$item->name->value] = $item; + } + + $this->items = $itemsAsHashMap; } public static function empty(): self { - return new self([]); + return new self(); + } + + public function merge(ImportNodes $other): self + { + return new self(...$this->items, ...$other->items); } public function withAddedImport(ImportNode $import): self @@ -51,7 +61,7 @@ public function withAddedImport(ImportNode $import): self throw new \Exception('@TODO: Duplicate Import ' . $name); } - return new self([...$this->items, ...[$name => $import]]); + return new self(...[...$this->items, ...[$name => $import]]); } public function get(string $name): ?ImportNode diff --git a/src/Parser/Ast/ModuleNode.php b/src/Parser/Ast/ModuleNode.php index e49b03f9..c3fcd1f1 100644 --- a/src/Parser/Ast/ModuleNode.php +++ b/src/Parser/Ast/ModuleNode.php @@ -22,6 +22,7 @@ namespace PackageFactory\ComponentEngine\Parser\Ast; +use PackageFactory\ComponentEngine\Parser\Parser\Module\ModuleParser; use PackageFactory\ComponentEngine\Parser\Source\Source; use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; @@ -30,7 +31,7 @@ final class ModuleNode implements \JsonSerializable { - private function __construct( + public function __construct( public readonly ImportNodes $imports, public readonly ExportNodes $exports, ) { @@ -38,11 +39,7 @@ private function __construct( public static function fromString(string $moduleAsString): self { - return self::fromTokens( - Tokenizer::fromSource( - Source::fromString($moduleAsString) - )->getIterator() - ); + return ModuleParser::parseFromString($moduleAsString); } /** diff --git a/src/Parser/Ast/PropertyDeclarationNode.php b/src/Parser/Ast/PropertyDeclarationNode.php index b1e2ab38..2d176ea3 100644 --- a/src/Parser/Ast/PropertyDeclarationNode.php +++ b/src/Parser/Ast/PropertyDeclarationNode.php @@ -28,7 +28,7 @@ final class PropertyDeclarationNode implements \JsonSerializable { - private function __construct( + public function __construct( public readonly string $name, public readonly TypeReferenceNode $type ) { diff --git a/src/Parser/Ast/PropertyDeclarationNodes.php b/src/Parser/Ast/PropertyDeclarationNodes.php index 12279ed5..c4a87544 100644 --- a/src/Parser/Ast/PropertyDeclarationNodes.php +++ b/src/Parser/Ast/PropertyDeclarationNodes.php @@ -33,18 +33,23 @@ final class PropertyDeclarationNodes implements \JsonSerializable */ public readonly array $items; - /** - * @param array $items - */ - private function __construct( - array $items + public function __construct( + PropertyDeclarationNode ...$items ) { - $this->items = $items; + $itemsAsHashMap = []; + foreach ($items as $item) { + if (array_key_exists($item->name, $itemsAsHashMap)) { + throw new \Exception('@TODO: Duplicate Property Declaration ' . $item->name); + } + $itemsAsHashMap[$item->name] = $item; + } + + $this->items = $itemsAsHashMap; } public static function empty(): self { - return new self([]); + return new self(); } /** @@ -85,7 +90,7 @@ public function withAddedPropertyDeclarationNode( throw new \Exception('@TODO: Duplicate Property Declaration ' . $name); } - return new self([...$this->items, ...[$name => $propertyDeclarationNode]]); + return new self(...[...$this->items, ...[$name => $propertyDeclarationNode]]); } public function getPropertyDeclarationNodeOfName(string $name): ?PropertyDeclarationNode diff --git a/src/Parser/Ast/StructDeclarationNode.php b/src/Parser/Ast/StructDeclarationNode.php index 24073f38..4a08500f 100644 --- a/src/Parser/Ast/StructDeclarationNode.php +++ b/src/Parser/Ast/StructDeclarationNode.php @@ -22,6 +22,7 @@ namespace PackageFactory\ComponentEngine\Parser\Ast; +use PackageFactory\ComponentEngine\Parser\Parser\StructDeclaration\StructDeclarationParser; use PackageFactory\ComponentEngine\Parser\Source\Source; use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; @@ -30,7 +31,7 @@ final class StructDeclarationNode implements \JsonSerializable { - private function __construct( + public function __construct( public readonly string $structName, public readonly PropertyDeclarationNodes $propertyDeclarations ) { @@ -38,11 +39,7 @@ private function __construct( public static function fromString(string $structDeclarationAsString): self { - return self::fromTokens( - Tokenizer::fromSource( - Source::fromString($structDeclarationAsString) - )->getIterator() - ); + return StructDeclarationParser::parseFromString($structDeclarationAsString); } /** diff --git a/src/Parser/Ast/TypeReferenceNode.php b/src/Parser/Ast/TypeReferenceNode.php index f4db1e7c..ccd70cbc 100644 --- a/src/Parser/Ast/TypeReferenceNode.php +++ b/src/Parser/Ast/TypeReferenceNode.php @@ -30,7 +30,7 @@ final class TypeReferenceNode implements \JsonSerializable { - private function __construct( + public function __construct( public readonly string $name, public readonly bool $isArray, public readonly bool $isOptional diff --git a/src/Parser/Parser/ComponentDeclaration/ComponentDeclarationParser.php b/src/Parser/Parser/ComponentDeclaration/ComponentDeclarationParser.php new file mode 100644 index 00000000..3458af0a --- /dev/null +++ b/src/Parser/Parser/ComponentDeclaration/ComponentDeclarationParser.php @@ -0,0 +1,78 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Parser\Parser\ComponentDeclaration; + +use PackageFactory\ComponentEngine\Parser\Ast\ComponentDeclarationNode; +use PackageFactory\ComponentEngine\Parser\Parser\Expression\ExpressionParser; +use PackageFactory\ComponentEngine\Parser\Parser\ParseFromString; +use PackageFactory\ComponentEngine\Parser\Parser\PropertyDeclaration\PropertyDeclarationParser; +use PackageFactory\ComponentEngine\Parser\Parser\UtilityParser; +use Parsica\Parsica\Parser; + +use function Parsica\Parsica\char; +use function Parsica\Parsica\collect; +use function Parsica\Parsica\skipSpace; + +/** + * @template T + */ +final class ComponentDeclarationParser +{ + /** @var ?Parser */ + private static ?Parser $instance = null; + + public static function parseFromString(string $string): ComponentDeclarationNode + { + return self::get()->thenEof()->tryString($string)->output(); + } + + /** @return Parser */ + public static function get(): Parser + { + return self::$instance ??= self::build(); + } + + /** @return Parser */ + private static function build(): Parser + { + return collect( + UtilityParser::keyword('component'), + skipSpace(), + UtilityParser::identifier(), + skipSpace(), + char('{'), + skipSpace(), + PropertyDeclarationParser::get(), + skipSpace(), + UtilityParser::keyword('return'), + skipSpace(), + ExpressionParser::get(), + skipSpace(), + char('}') + )->map(fn ($collected) => new ComponentDeclarationNode( + $collected[2], + $collected[6], + $collected[10] + )); + } +} diff --git a/src/Parser/Parser/EnumDeclaration/EnumDeclarationParser.php b/src/Parser/Parser/EnumDeclaration/EnumDeclarationParser.php new file mode 100644 index 00000000..3864d469 --- /dev/null +++ b/src/Parser/Parser/EnumDeclaration/EnumDeclarationParser.php @@ -0,0 +1,66 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Parser\Parser\EnumDeclaration; + +use PackageFactory\ComponentEngine\Parser\Ast\EnumDeclarationNode; +use PackageFactory\ComponentEngine\Parser\Parser\EnumMemberDeclaration\EnumMemberDeclarationParser; +use PackageFactory\ComponentEngine\Parser\Parser\UtilityParser; +use Parsica\Parsica\Parser; + +use function Parsica\Parsica\char; +use function Parsica\Parsica\collect; +use function Parsica\Parsica\skipSpace; + +final class EnumDeclarationParser +{ + private static ?Parser $instance = null; + + public static function parseFromString(string $string): EnumDeclarationNode + { + return self::get()->thenEof()->tryString($string)->output(); + } + + public static function get(): Parser + { + return self::$instance ??= self::build(); + } + + private static function build(): Parser + { + return collect( + UtilityParser::keyword('enum'), + skipSpace(), + UtilityParser::identifier(), + skipSpace(), + char('{'), + skipSpace(), + EnumMemberDeclarationParser::get(), + skipSpace(), + skipSpace(), + char('}') + )->map(fn ($collected) => new EnumDeclarationNode( + $collected[2], + $collected[6], + )); + } +} diff --git a/src/Parser/Parser/EnumMemberDeclaration/EnumMemberDeclarationParser.php b/src/Parser/Parser/EnumMemberDeclaration/EnumMemberDeclarationParser.php new file mode 100644 index 00000000..458a75d6 --- /dev/null +++ b/src/Parser/Parser/EnumMemberDeclaration/EnumMemberDeclarationParser.php @@ -0,0 +1,74 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Parser\Parser\EnumMemberDeclaration; + +use PackageFactory\ComponentEngine\Parser\Ast\EnumDeclarationNode; +use PackageFactory\ComponentEngine\Parser\Ast\EnumMemberDeclarationNode; +use PackageFactory\ComponentEngine\Parser\Ast\EnumMemberDeclarationNodes; +use PackageFactory\ComponentEngine\Parser\Ast\PropertyDeclarationNode; +use PackageFactory\ComponentEngine\Parser\Ast\PropertyDeclarationNodes; +use PackageFactory\ComponentEngine\Parser\Parser\NumberLiteral\NumberLiteralParser; +use PackageFactory\ComponentEngine\Parser\Parser\StringLiteral\StringLiteralParser; +use PackageFactory\ComponentEngine\Parser\Parser\TypeReference\TypeReferenceParser; +use PackageFactory\ComponentEngine\Parser\Parser\UtilityParser; +use Parsica\Parsica\Parser; + +use function Parsica\Parsica\any; +use function Parsica\Parsica\between; +use function Parsica\Parsica\char; +use function Parsica\Parsica\collect; +use function Parsica\Parsica\either; +use function Parsica\Parsica\many; +use function Parsica\Parsica\optional; +use function Parsica\Parsica\skipSpace; + +final class EnumMemberDeclarationParser +{ + private static ?Parser $instance = null; + + public static function get(): Parser + { + return self::$instance ??= self::build(); + } + + private static function build(): Parser + { + return many( + collect( + UtilityParser::identifier(), + skipSpace(), + optional( + between( + char('('), + char(')'), + either( + StringLiteralParser::get(), + NumberLiteralParser::get() + ) + ) + ), + skipSpace() + )->map(fn ($collected) => new EnumMemberDeclarationNode($collected[0], $collected[2])) + )->map(fn ($collected) => new EnumMemberDeclarationNodes(...$collected ? $collected : [])); + } +} diff --git a/src/Parser/Parser/Export/ExportParser.php b/src/Parser/Parser/Export/ExportParser.php new file mode 100644 index 00000000..c659bf4e --- /dev/null +++ b/src/Parser/Parser/Export/ExportParser.php @@ -0,0 +1,62 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Parser\Parser\Export; + +use PackageFactory\ComponentEngine\Parser\Ast\ExportNode; +use PackageFactory\ComponentEngine\Parser\Ast\ExportNodes; +use PackageFactory\ComponentEngine\Parser\Ast\StructDeclarationNode; +use PackageFactory\ComponentEngine\Parser\Parser\ComponentDeclaration\ComponentDeclarationParser; +use PackageFactory\ComponentEngine\Parser\Parser\EnumDeclaration\EnumDeclarationParser; +use PackageFactory\ComponentEngine\Parser\Parser\StructDeclaration\StructDeclarationParser; +use PackageFactory\ComponentEngine\Parser\Parser\UtilityParser; +use Parsica\Parsica\Parser; + +use function Parsica\Parsica\any; +use function Parsica\Parsica\collect; +use function Parsica\Parsica\many; +use function Parsica\Parsica\skipSpace; + +final class ExportParser +{ + private static ?Parser $instance = null; + + public static function get(): Parser + { + return self::$instance ??= self::build(); + } + + private static function build(): Parser + { + return collect( + skipSpace(), + UtilityParser::keyword('export'), + skipSpace(), + any( + ComponentDeclarationParser::get(), + EnumDeclarationParser::get(), + StructDeclarationParser::get() + ), + skipSpace() + )->map(fn ($collected) => new ExportNode($collected[3])); + } +} diff --git a/src/Parser/Parser/Expression/ExpressionParser.php b/src/Parser/Parser/Expression/ExpressionParser.php index bff72985..f1b931da 100644 --- a/src/Parser/Parser/Expression/ExpressionParser.php +++ b/src/Parser/Parser/Expression/ExpressionParser.php @@ -43,9 +43,9 @@ final class ExpressionParser { - public static function parseFromString(string $expressionAsString): ExpressionNode + public static function parseFromString(string $string): ExpressionNode { - return self::get()->thenEof()->tryString($expressionAsString)->output(); + return self::get()->thenEof()->tryString($string)->output(); } /** @return Parser */ diff --git a/src/Parser/Parser/Import/ImportParser.php b/src/Parser/Parser/Import/ImportParser.php new file mode 100644 index 00000000..64de0866 --- /dev/null +++ b/src/Parser/Parser/Import/ImportParser.php @@ -0,0 +1,73 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Parser\Parser\Import; + +use PackageFactory\ComponentEngine\Parser\Ast\IdentifierNode; +use PackageFactory\ComponentEngine\Parser\Ast\ImportNode; +use PackageFactory\ComponentEngine\Parser\Ast\ImportNodes; +use PackageFactory\ComponentEngine\Parser\Parser\Identifier\IdentifierParser; +use PackageFactory\ComponentEngine\Parser\Parser\UtilityParser; +use PackageFactory\ComponentEngine\Parser\Source\Path; +use Parsica\Parsica\Parser; + +use function Parsica\Parsica\between; +use function Parsica\Parsica\char; +use function Parsica\Parsica\collect; +use function Parsica\Parsica\sepBy; +use function Parsica\Parsica\sepBy1; +use function Parsica\Parsica\skipSpace; + +final class ImportParser +{ + /** @return Parser */ + public static function get(Path $path): Parser + { + return collect( + skipSpace(), + UtilityParser::keyword('from'), + skipSpace(), + UtilityParser::quotedStringContents(), + skipSpace(), + UtilityParser::keyword('import'), + skipSpace(), + char('{'), + skipSpace(), + sepBy1( + between(skipSpace(), skipSpace(), char(',')), + IdentifierParser::get(), + ), + skipSpace(), + char('}'), + skipSpace(), + )->map(fn ($collected) => new ImportNodes( + ...array_map( + fn (IdentifierNode $name) => new ImportNode( + sourcePath: $path, + path: $collected[3], + name: $name + ), + $collected[9], + ) + )); + } +} diff --git a/src/Parser/Parser/Module/ModuleParser.php b/src/Parser/Parser/Module/ModuleParser.php new file mode 100644 index 00000000..134a7cde --- /dev/null +++ b/src/Parser/Parser/Module/ModuleParser.php @@ -0,0 +1,75 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Parser\Parser\Module; + +use PackageFactory\ComponentEngine\Parser\Ast\ExportNode; +use PackageFactory\ComponentEngine\Parser\Ast\ExportNodes; +use PackageFactory\ComponentEngine\Parser\Ast\ImportNode; +use PackageFactory\ComponentEngine\Parser\Ast\ImportNodes; +use PackageFactory\ComponentEngine\Parser\Ast\ModuleNode; +use PackageFactory\ComponentEngine\Parser\Parser\Export\ExportParser; +use PackageFactory\ComponentEngine\Parser\Parser\Import\ImportParser; +use PackageFactory\ComponentEngine\Parser\Source\Path; +use Parsica\Parsica\Parser; + +use function Parsica\Parsica\either; +use function Parsica\Parsica\many; +use function Parsica\Parsica\skipSpace; + +final class ModuleParser +{ + private static ?Parser $instance = null; + + public static function fromPath(Path $path): ModuleNode + { + + } + + public static function parseFromString(string $string): ModuleNode + { + return self::get(Path::createMemory())->thenEof()->tryString($string)->output(); + } + + public static function get(Path $path): Parser + { + return many( + either( + ImportParser::get($path), + ExportParser::get() + ) + )->map(function ($collected) { + $importNodes = ImportNodes::empty(); + $exportNodes = []; + foreach ($collected as $item) { + match ($item::class) { + ImportNodes::class => $importNodes = $importNodes->merge($item), + ExportNode::class => $exportNodes[] = $item + }; + } + return new ModuleNode( + $importNodes, + new ExportNodes(...$exportNodes) + ); + }); + } +} diff --git a/src/Parser/Parser/PropertyDeclaration/PropertyDeclarationParser.php b/src/Parser/Parser/PropertyDeclaration/PropertyDeclarationParser.php new file mode 100644 index 00000000..c485c1f7 --- /dev/null +++ b/src/Parser/Parser/PropertyDeclaration/PropertyDeclarationParser.php @@ -0,0 +1,58 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Parser\Parser\PropertyDeclaration; + +use PackageFactory\ComponentEngine\Parser\Ast\PropertyDeclarationNode; +use PackageFactory\ComponentEngine\Parser\Ast\PropertyDeclarationNodes; +use PackageFactory\ComponentEngine\Parser\Parser\TypeReference\TypeReferenceParser; +use PackageFactory\ComponentEngine\Parser\Parser\UtilityParser; +use Parsica\Parsica\Parser; + +use function Parsica\Parsica\char; +use function Parsica\Parsica\collect; +use function Parsica\Parsica\many; +use function Parsica\Parsica\skipSpace; + +final class PropertyDeclarationParser +{ + private static ?Parser $instance = null; + + public static function get(): Parser + { + return self::$instance ??= self::build(); + } + + private static function build(): Parser + { + return many( + collect( + UtilityParser::identifier(), + skipSpace(), + char(':'), + skipSpace(), + TypeReferenceParser::get(), + skipSpace() + )->map(fn ($collected) => new PropertyDeclarationNode($collected[0], $collected[4])) + )->map(fn ($collected) => new PropertyDeclarationNodes(...$collected ? $collected : [])); + } +} diff --git a/src/Parser/Parser/StructDeclaration/StructDeclarationParser.php b/src/Parser/Parser/StructDeclaration/StructDeclarationParser.php new file mode 100644 index 00000000..4e2fb33d --- /dev/null +++ b/src/Parser/Parser/StructDeclaration/StructDeclarationParser.php @@ -0,0 +1,66 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Parser\Parser\StructDeclaration; + +use PackageFactory\ComponentEngine\Parser\Ast\StructDeclarationNode; +use PackageFactory\ComponentEngine\Parser\Parser\PropertyDeclaration\PropertyDeclarationParser; +use PackageFactory\ComponentEngine\Parser\Parser\UtilityParser; +use Parsica\Parsica\Parser; + +use function Parsica\Parsica\char; +use function Parsica\Parsica\collect; +use function Parsica\Parsica\skipSpace; + +final class StructDeclarationParser +{ + private static ?Parser $instance = null; + + public static function parseFromString(string $string): StructDeclarationNode + { + return self::get()->thenEof()->tryString($string)->output(); + } + + public static function get(): Parser + { + return self::$instance ??= self::build(); + } + + private static function build(): Parser + { + return collect( + UtilityParser::keyword('struct'), + skipSpace(), + UtilityParser::identifier(), + skipSpace(), + char('{'), + skipSpace(), + PropertyDeclarationParser::get(), + skipSpace(), + char('}'), + skipSpace(), + )->map(fn ($collected) => new StructDeclarationNode( + $collected[2], + $collected[6], + )); + } +} diff --git a/src/Parser/Parser/TypeReference/TypeReferenceParser.php b/src/Parser/Parser/TypeReference/TypeReferenceParser.php new file mode 100644 index 00000000..c9a92571 --- /dev/null +++ b/src/Parser/Parser/TypeReference/TypeReferenceParser.php @@ -0,0 +1,57 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Parser\Parser\TypeReference; + +use PackageFactory\ComponentEngine\Parser\Ast\TypeReferenceNode; +use PackageFactory\ComponentEngine\Parser\Parser\ParseFromString; +use PackageFactory\ComponentEngine\Parser\Parser\UtilityParser; +use Parsica\Parsica\Parser; + +use function Parsica\Parsica\assemble; +use function Parsica\Parsica\char; +use function Parsica\Parsica\collect; +use function Parsica\Parsica\optional; +use function Parsica\Parsica\skipHSpace; + +final class TypeReferenceParser +{ + private static ?Parser $instance = null; + + public static function get(): Parser + { + return self::$instance ??= self::build(); + } + + private static function build(): Parser + { + return collect( + optional(char('?')), + UtilityParser::identifier(), + optional(assemble(char('['), skipHSpace(), char(']'))) + )->map(fn ($collected) => new TypeReferenceNode( + name: $collected[1], + isArray: (bool)$collected[2], + isOptional: (bool)$collected[0], + )); + } +} From ad4b62a269a8a3cf05facc5435a20f3175f5a486 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Fri, 19 May 2023 22:20:00 +0200 Subject: [PATCH 13/20] WIP: Parsica implement TagAttributes, Refine `SourcePath` handling enable functional tests unit test pass functional test 15 fail, because of comments and the space and new line handling of TextNode --- src/Parser/Ast/ImportNodes.php | 3 +- src/Parser/Ast/ModuleNode.php | 4 +- .../Parser/Attribute/AttributeParser.php | 33 ++++++++++- src/Parser/Parser/Import/ImportParser.php | 56 ++++++++++--------- src/Parser/Parser/Module/ModuleParser.php | 16 +++--- src/Parser/Parser/UtilityParser.php | 22 ++++++++ test/Integration/ParserIntegrationTest.php | 14 ++++- .../PhpTranspilerIntegrationTest.php | 17 +++--- .../Module/Loader/Fixtures/DummyLoader.php | 2 +- 9 files changed, 114 insertions(+), 53 deletions(-) diff --git a/src/Parser/Ast/ImportNodes.php b/src/Parser/Ast/ImportNodes.php index 670bdd45..f3cca333 100644 --- a/src/Parser/Ast/ImportNodes.php +++ b/src/Parser/Ast/ImportNodes.php @@ -50,7 +50,8 @@ public static function empty(): self public function merge(ImportNodes $other): self { - return new self(...$this->items, ...$other->items); + // without array_values we would silently ignore double named imports + return new self(...array_values($this->items), ...array_values($other->items)); } public function withAddedImport(ImportNode $import): self diff --git a/src/Parser/Ast/ModuleNode.php b/src/Parser/Ast/ModuleNode.php index c3fcd1f1..bff83a10 100644 --- a/src/Parser/Ast/ModuleNode.php +++ b/src/Parser/Ast/ModuleNode.php @@ -23,11 +23,9 @@ namespace PackageFactory\ComponentEngine\Parser\Ast; use PackageFactory\ComponentEngine\Parser\Parser\Module\ModuleParser; -use PackageFactory\ComponentEngine\Parser\Source\Source; -use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Tokenizer; +use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; final class ModuleNode implements \JsonSerializable { diff --git a/src/Parser/Parser/Attribute/AttributeParser.php b/src/Parser/Parser/Attribute/AttributeParser.php index 3f0d7a66..31f7ed81 100644 --- a/src/Parser/Parser/Attribute/AttributeParser.php +++ b/src/Parser/Parser/Attribute/AttributeParser.php @@ -22,10 +22,20 @@ namespace PackageFactory\ComponentEngine\Parser\Parser\Attribute; +use PackageFactory\ComponentEngine\Parser\Ast\AttributeNode; use PackageFactory\ComponentEngine\Parser\Ast\AttributeNodes; +use PackageFactory\ComponentEngine\Parser\Parser\Expression\ExpressionParser; +use PackageFactory\ComponentEngine\Parser\Parser\StringLiteral\StringLiteralParser; +use PackageFactory\ComponentEngine\Parser\Parser\UtilityParser; use Parsica\Parsica\Parser; -use function Parsica\Parsica\succeed; +use function Parsica\Parsica\between; +use function Parsica\Parsica\char; +use function Parsica\Parsica\collect; +use function Parsica\Parsica\either; +use function Parsica\Parsica\many; +use function Parsica\Parsica\skipSpace; + final class AttributeParser { private static ?Parser $instance = null; @@ -37,6 +47,25 @@ public static function get(): Parser private static function build(): Parser { - return succeed()->map(fn () => new AttributeNodes()); + return many( + collect( + skipSpace(), + self::attributeIdentifier(), + char('='), + either( + StringLiteralParser::get(), + between( + char('{'), + char('}'), + ExpressionParser::get() + ) + ) + )->map(fn ($collected) => new AttributeNode($collected[1], $collected[3])) + )->map(fn ($collected) => new AttributeNodes(...$collected ? $collected : [])); + } + + private static function attributeIdentifier(): Parser + { + return UtilityParser::identifier(); } } diff --git a/src/Parser/Parser/Import/ImportParser.php b/src/Parser/Parser/Import/ImportParser.php index 64de0866..08d3ad3f 100644 --- a/src/Parser/Parser/Import/ImportParser.php +++ b/src/Parser/Parser/Import/ImportParser.php @@ -33,41 +33,43 @@ use function Parsica\Parsica\between; use function Parsica\Parsica\char; use function Parsica\Parsica\collect; -use function Parsica\Parsica\sepBy; use function Parsica\Parsica\sepBy1; use function Parsica\Parsica\skipSpace; final class ImportParser { /** @return Parser */ - public static function get(Path $path): Parser + public static function get(): Parser { - return collect( - skipSpace(), - UtilityParser::keyword('from'), - skipSpace(), - UtilityParser::quotedStringContents(), - skipSpace(), - UtilityParser::keyword('import'), - skipSpace(), - char('{'), - skipSpace(), - sepBy1( - between(skipSpace(), skipSpace(), char(',')), - IdentifierParser::get(), - ), - skipSpace(), - char('}'), - skipSpace(), - )->map(fn ($collected) => new ImportNodes( - ...array_map( - fn (IdentifierNode $name) => new ImportNode( - sourcePath: $path, - path: $collected[3], - name: $name + return UtilityParser::mapWithPath( + collect( + skipSpace(), + UtilityParser::keyword('from'), + skipSpace(), + UtilityParser::quotedStringContents(), + skipSpace(), + UtilityParser::keyword('import'), + skipSpace(), + char('{'), + skipSpace(), + sepBy1( + between(skipSpace(), skipSpace(), char(',')), + IdentifierParser::get(), ), - $collected[9], + skipSpace(), + char('}'), + skipSpace(), + ), + fn (array $collected, Path $sourcePath) => new ImportNodes( + ...array_map( + fn (IdentifierNode $name) => new ImportNode( + sourcePath: $sourcePath, + path: $collected[3], + name: $name + ), + $collected[9], + ) ) - )); + ); } } diff --git a/src/Parser/Parser/Module/ModuleParser.php b/src/Parser/Parser/Module/ModuleParser.php index 134a7cde..3a26e8ee 100644 --- a/src/Parser/Parser/Module/ModuleParser.php +++ b/src/Parser/Parser/Module/ModuleParser.php @@ -24,37 +24,35 @@ use PackageFactory\ComponentEngine\Parser\Ast\ExportNode; use PackageFactory\ComponentEngine\Parser\Ast\ExportNodes; -use PackageFactory\ComponentEngine\Parser\Ast\ImportNode; use PackageFactory\ComponentEngine\Parser\Ast\ImportNodes; use PackageFactory\ComponentEngine\Parser\Ast\ModuleNode; use PackageFactory\ComponentEngine\Parser\Parser\Export\ExportParser; use PackageFactory\ComponentEngine\Parser\Parser\Import\ImportParser; -use PackageFactory\ComponentEngine\Parser\Source\Path; use Parsica\Parsica\Parser; +use Parsica\Parsica\Stream; use function Parsica\Parsica\either; use function Parsica\Parsica\many; -use function Parsica\Parsica\skipSpace; final class ModuleParser { private static ?Parser $instance = null; - public static function fromPath(Path $path): ModuleNode + public static function parseFromStream(Stream $stream): ModuleNode { - + return self::get()->thenEof()->try($stream)->output(); } public static function parseFromString(string $string): ModuleNode { - return self::get(Path::createMemory())->thenEof()->tryString($string)->output(); + return self::get()->thenEof()->tryString($string)->output(); } - public static function get(Path $path): Parser + public static function get(): Parser { - return many( + return self::$instance ??= many( either( - ImportParser::get($path), + ImportParser::get(), ExportParser::get() ) )->map(function ($collected) { diff --git a/src/Parser/Parser/UtilityParser.php b/src/Parser/Parser/UtilityParser.php index f28e3d6c..00a64c1f 100644 --- a/src/Parser/Parser/UtilityParser.php +++ b/src/Parser/Parser/UtilityParser.php @@ -22,6 +22,7 @@ namespace PackageFactory\ComponentEngine\Parser\Parser; +use PackageFactory\ComponentEngine\Parser\Source\Path; use Parsica\Parsica\Internal\Fail; use Parsica\Parsica\Internal\Succeed; use Parsica\Parsica\Parser; @@ -107,4 +108,25 @@ public static function strings(array $strings): Parser return new Fail('strings', $stream); }); } + + /** + * Map a function over the parser (which in turn maps it over the result). + * + * @template T1 + * @template T2 + * @psalm-param Parser $parser + * @psalm-param callable(T1, Path) : T2 $transformWithPath + * @psalm-return Parser + */ + public static function mapWithPath(Parser $parser, callable $transformWithPath): Parser + { + return Parser::make($parser->getLabel(), function (Stream $stream) use($parser, $transformWithPath) { + $fileName = $stream->position()->filename(); + $path = match ($fileName) { + '' => Path::createMemory(), + default => Path::fromString($fileName) + }; + return $parser->run($stream)->map(fn ($output) => $transformWithPath($output, $path)); + }); + } } diff --git a/test/Integration/ParserIntegrationTest.php b/test/Integration/ParserIntegrationTest.php index bc79713b..5af617f5 100644 --- a/test/Integration/ParserIntegrationTest.php +++ b/test/Integration/ParserIntegrationTest.php @@ -22,9 +22,11 @@ namespace PackageFactory\ComponentEngine\Test\Integration; -use PackageFactory\ComponentEngine\Parser\Ast\ModuleNode; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Tokenizer; +use PackageFactory\ComponentEngine\Parser\Parser\Module\ModuleParser; use PackageFactory\ComponentEngine\Parser\Source\Source; +use PackageFactory\ComponentEngine\Parser\Tokenizer\Tokenizer; +use Parsica\Parsica\Internal\Position; +use Parsica\Parsica\StringStream; use PHPUnit\Framework\TestCase; final class ParserIntegrationTest extends TestCase @@ -61,6 +63,7 @@ public function astExamples(): array */ public function testParser(string $example): void { + // @todo remove token tests $source = Source::fromFile(__DIR__ . '/Examples/' . $example . '/' . $example . '.afx'); $tokenizer = Tokenizer::fromSource($source); $astAsJsonString = file_get_contents(__DIR__ . '/Examples/' . $example . '/' . $example . '.ast.json'); @@ -68,7 +71,12 @@ public function testParser(string $example): void $expected = json_decode($astAsJsonString, true); - $module = ModuleNode::fromTokens($tokenizer->getIterator()); + $fileName = __DIR__ . '/Examples/' . $example . '/' . $example . '.afx'; + $stream = new StringStream( + file_get_contents($fileName) ?: throw new \RuntimeException('could not load file.'), + Position::initial($fileName) + ); + $module = ModuleParser::parseFromStream($stream); $moduleAsJson = json_encode($module); assert($moduleAsJson !== false); diff --git a/test/Integration/PhpTranspilerIntegrationTest.php b/test/Integration/PhpTranspilerIntegrationTest.php index 945a4c1f..39b03f52 100644 --- a/test/Integration/PhpTranspilerIntegrationTest.php +++ b/test/Integration/PhpTranspilerIntegrationTest.php @@ -24,18 +24,18 @@ use PackageFactory\ComponentEngine\Module\Loader\ModuleFile\ModuleFileLoader; use PackageFactory\ComponentEngine\Parser\Ast\EnumDeclarationNode; -use PackageFactory\ComponentEngine\Parser\Ast\ModuleNode; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Tokenizer; -use PackageFactory\ComponentEngine\Parser\Source\Source; -use PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Scope\Fixtures\DummyScope; +use PackageFactory\ComponentEngine\Parser\Parser\Module\ModuleParser; use PackageFactory\ComponentEngine\Target\Php\Transpiler\Module\ModuleTranspiler; use PackageFactory\ComponentEngine\Test\Unit\Target\Php\Transpiler\Module\ModuleTestStrategy; +use PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Scope\Fixtures\DummyScope; use PackageFactory\ComponentEngine\TypeSystem\Type\BooleanType\BooleanType; use PackageFactory\ComponentEngine\TypeSystem\Type\EnumType\EnumStaticType; use PackageFactory\ComponentEngine\TypeSystem\Type\EnumType\EnumType; use PackageFactory\ComponentEngine\TypeSystem\Type\NumberType\NumberType; use PackageFactory\ComponentEngine\TypeSystem\Type\SlotType\SlotType; use PackageFactory\ComponentEngine\TypeSystem\Type\StringType\StringType; +use Parsica\Parsica\Internal\Position; +use Parsica\Parsica\StringStream; use PHPUnit\Framework\TestCase; final class PhpTranspilerIntegrationTest extends TestCase @@ -71,9 +71,12 @@ public function transpilerExamples(): array */ public function testTranspiler(string $example): void { - $source = Source::fromFile(__DIR__ . '/Examples/' . $example . '/' . $example . '.afx'); - $tokenizer = Tokenizer::fromSource($source); - $module = ModuleNode::fromTokens($tokenizer->getIterator()); + $fileName = __DIR__ . '/Examples/' . $example . '/' . $example . '.afx'; + $stream = new StringStream( + file_get_contents($fileName) ?: throw new \RuntimeException('could not load file.'), + Position::initial($fileName) + ); + $module = ModuleParser::parseFromStream($stream); $expected = file_get_contents(__DIR__ . '/Examples/' . $example . '/' . $example . '.php'); diff --git a/test/Unit/Module/Loader/Fixtures/DummyLoader.php b/test/Unit/Module/Loader/Fixtures/DummyLoader.php index f74a6a80..2a687a67 100644 --- a/test/Unit/Module/Loader/Fixtures/DummyLoader.php +++ b/test/Unit/Module/Loader/Fixtures/DummyLoader.php @@ -43,7 +43,7 @@ public function resolveTypeOfImport(ImportNode $importNode): TypeInterface } throw new \Exception( - '[DummyLoader] Cannot import "' . $importNode->name->value . '" from "' . $importNode->source->path->value . '"' + '[DummyLoader] Cannot import "' . $importNode->name->value . '" from "' . $importNode->sourcePath->value . '"' ); } From 893015d3b5ea670c43e7f9824cf6a011d69068c4 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 20 May 2023 19:23:30 +0200 Subject: [PATCH 14/20] WIP: Parsica add correct whitespace handling in html tags --- .../Parser/TagContent/TagContentParser.php | 11 ++++--- src/Parser/Parser/Text/TextParser.php | 31 +++++++++++++------ 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/src/Parser/Parser/TagContent/TagContentParser.php b/src/Parser/Parser/TagContent/TagContentParser.php index 2dd75044..53770b49 100644 --- a/src/Parser/Parser/TagContent/TagContentParser.php +++ b/src/Parser/Parser/TagContent/TagContentParser.php @@ -32,6 +32,7 @@ use function Parsica\Parsica\any; use function Parsica\Parsica\char; use function Parsica\Parsica\many; +use function Parsica\Parsica\skipSpace; final class TagContentParser { @@ -44,9 +45,11 @@ public static function get(): Parser private static function build(): Parser { - return many( - self::tagContent() - )->map(fn ($collected) => new TagContentNodes(...$collected ? $collected : [])); + return skipSpace()->sequence( + many( + self::tagContent() + )->map(fn ($collected) => new TagContentNodes(...$collected ? array_filter($collected) : [])) + ); } private static function tagContent(): Parser @@ -55,6 +58,6 @@ private static function tagContent(): Parser TagParser::get(), TextParser::get(), char('{')->followedBy(ExpressionParser::get())->thenIgnore(char('}')), - )->map(fn ($item) => new TagContentNode($item)); + )->map(fn ($item) => $item ? new TagContentNode($item) : null); } } diff --git a/src/Parser/Parser/Text/TextParser.php b/src/Parser/Parser/Text/TextParser.php index e0e5f2c5..9f5f0e20 100644 --- a/src/Parser/Parser/Text/TextParser.php +++ b/src/Parser/Parser/Text/TextParser.php @@ -25,20 +25,33 @@ use PackageFactory\ComponentEngine\Parser\Ast\TextNode; use Parsica\Parsica\Parser; +use function Parsica\Parsica\anySingle; +use function Parsica\Parsica\collect; +use function Parsica\Parsica\lookAhead; use function Parsica\Parsica\takeWhile1; final class TextParser { - private static ?Parser $instance = null; - public static function get(): Parser { - return self::$instance ??= self::build(); - } - - private static function build(): Parser - { - // @todo space handling and valid chars - return takeWhile1(fn ($char) => $char !== '<' && $char !== '{')->map(fn ($text) => new TextNode($text)); + return + collect( + takeWhile1( + // @todo handling of valid chars + fn ($char) => $char !== '<' && $char !== '{' + ), + lookAhead(anySingle()->append(anySingle())) + ) + ->map(function ($collected) { + [$text, $nextChars] = $collected; + if ($nextChars === ' Date: Sat, 20 May 2023 19:49:49 +0200 Subject: [PATCH 15/20] WIP: Parsica simple comment handling in few places to make tests happy ;) --- src/Parser/Parser/Expression/ExpressionParser.php | 4 +++- src/Parser/Parser/Module/ModuleParser.php | 5 +++-- src/Parser/Parser/UtilityParser.php | 13 +++++++++++++ 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/Parser/Parser/Expression/ExpressionParser.php b/src/Parser/Parser/Expression/ExpressionParser.php index f1b931da..971c9a8f 100644 --- a/src/Parser/Parser/Expression/ExpressionParser.php +++ b/src/Parser/Parser/Expression/ExpressionParser.php @@ -37,6 +37,7 @@ use PackageFactory\ComponentEngine\Parser\Parser\TemplateLiteral\TemplateLiteralParser; use PackageFactory\ComponentEngine\Parser\Parser\TernaryOperation\TernaryOperationParser; use PackageFactory\ComponentEngine\Parser\Parser\UnaryOperation\UnaryOperationParser; +use PackageFactory\ComponentEngine\Parser\Parser\UtilityParser; use Parsica\Parsica\Parser; use function Parsica\Parsica\{any, between, either, pure, skipSpace}; @@ -51,7 +52,8 @@ public static function parseFromString(string $string): ExpressionNode /** @return Parser */ public static function get(Precedence $precedence = Precedence::SEQUENCE): Parser { - $expressionRootParser = between(skipSpace(), skipSpace(), any( + // @todo parens () are currently hard to implement in the current architecture + $expressionRootParser = between(UtilityParser::skipSpaceAndComments(), UtilityParser::skipSpaceAndComments(), any( NumberLiteralParser::get(), BooleanLiteralParser::get(), NullLiteralParser::get(), diff --git a/src/Parser/Parser/Module/ModuleParser.php b/src/Parser/Parser/Module/ModuleParser.php index 3a26e8ee..425a50bc 100644 --- a/src/Parser/Parser/Module/ModuleParser.php +++ b/src/Parser/Parser/Module/ModuleParser.php @@ -28,6 +28,7 @@ use PackageFactory\ComponentEngine\Parser\Ast\ModuleNode; use PackageFactory\ComponentEngine\Parser\Parser\Export\ExportParser; use PackageFactory\ComponentEngine\Parser\Parser\Import\ImportParser; +use PackageFactory\ComponentEngine\Parser\Parser\UtilityParser; use Parsica\Parsica\Parser; use Parsica\Parsica\Stream; @@ -50,7 +51,7 @@ public static function parseFromString(string $string): ModuleNode public static function get(): Parser { - return self::$instance ??= many( + return self::$instance ??= UtilityParser::skipSpaceAndComments()->then(many( either( ImportParser::get(), ExportParser::get() @@ -68,6 +69,6 @@ public static function get(): Parser $importNodes, new ExportNodes(...$exportNodes) ); - }); + })); } } diff --git a/src/Parser/Parser/UtilityParser.php b/src/Parser/Parser/UtilityParser.php index 00a64c1f..991ec529 100644 --- a/src/Parser/Parser/UtilityParser.php +++ b/src/Parser/Parser/UtilityParser.php @@ -34,9 +34,12 @@ use function Parsica\Parsica\anySingle; use function Parsica\Parsica\append; use function Parsica\Parsica\char; +use function Parsica\Parsica\either; +use function Parsica\Parsica\isSpace; use function Parsica\Parsica\oneOfS; use function Parsica\Parsica\string; use function Parsica\Parsica\takeWhile; +use function Parsica\Parsica\takeWhile1; use function Parsica\Parsica\zeroOrMore; final class UtilityParser @@ -51,6 +54,16 @@ public static function keyword(string $keyword): Parser return string($keyword)->notFollowedBy(alphaNumChar()); } + public static function skipSpaceAndComments(): Parser + { + return zeroOrMore( + either( + takeWhile1(isSpace()), + char('#')->then(takeWhile(fn ($char) => $char !== "\n")) + ) + )->voidLeft(null); + } + /** * @return Parser */ From cc2c9c71604c8a68d7029afa8b83a88206d013a9 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 21 May 2023 12:49:20 +0200 Subject: [PATCH 16/20] WIP: Parsica expression brace handling () --- .../Parser/Expression/ExpressionParser.php | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/Parser/Parser/Expression/ExpressionParser.php b/src/Parser/Parser/Expression/ExpressionParser.php index 971c9a8f..5f3829d2 100644 --- a/src/Parser/Parser/Expression/ExpressionParser.php +++ b/src/Parser/Parser/Expression/ExpressionParser.php @@ -40,7 +40,7 @@ use PackageFactory\ComponentEngine\Parser\Parser\UtilityParser; use Parsica\Parsica\Parser; -use function Parsica\Parsica\{any, between, either, pure, skipSpace}; +use function Parsica\Parsica\{any, between, char, either, pure, skipSpace}; final class ExpressionParser { @@ -52,8 +52,7 @@ public static function parseFromString(string $string): ExpressionNode /** @return Parser */ public static function get(Precedence $precedence = Precedence::SEQUENCE): Parser { - // @todo parens () are currently hard to implement in the current architecture - $expressionRootParser = between(UtilityParser::skipSpaceAndComments(), UtilityParser::skipSpaceAndComments(), any( + $expressionRootParser = any( NumberLiteralParser::get(), BooleanLiteralParser::get(), NullLiteralParser::get(), @@ -63,10 +62,17 @@ public static function get(Precedence $precedence = Precedence::SEQUENCE): Parse IdentifierParser::get(), TemplateLiteralParser::get(), UnaryOperationParser::get() - )); + ); - return $expressionRootParser->bind( - fn ($expressionRoot) => self::continueParsingWhilePrecedence(new ExpressionNode($expressionRoot), $precedence) + return UtilityParser::skipSpaceAndComments()->sequence( + either( + char('(')->bind( + fn () => self::get()->thenIgnore(char(')'))->thenIgnore(UtilityParser::skipSpaceAndComments()) + ), + $expressionRootParser->thenIgnore(UtilityParser::skipSpaceAndComments())->bind( + fn ($expressionRoot) => self::continueParsingWhilePrecedence(new ExpressionNode($expressionRoot), $precedence) + ), + ) ); } @@ -78,7 +84,7 @@ private static function continueParsingWhilePrecedence(ExpressionNode $expressio AccessParser::get($expressionNode), TernaryOperationParser::get($expressionNode) ) - ->thenIgnore(skipSpace()) + ->thenIgnore(UtilityParser::skipSpaceAndComments()) ->bind( fn ($expressionRoot) => self::continueParsingWhilePrecedence(new ExpressionNode($expressionRoot), $precedence) ); From 1e87337d305967854701bbe4932753610f8b7f21 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 21 May 2023 12:57:32 +0200 Subject: [PATCH 17/20] WIP: Parsica match node with multiple expressions as match arm ALL TEST PASS :D ~90ms --- .../Parser/Expression/ExpressionParser.php | 2 +- .../Parser/Expression/ExpressionsParser.php | 47 +++++++++++++++++++ src/Parser/Parser/MatchArm/MatchArmParser.php | 7 ++- 3 files changed, 51 insertions(+), 5 deletions(-) create mode 100644 src/Parser/Parser/Expression/ExpressionsParser.php diff --git a/src/Parser/Parser/Expression/ExpressionParser.php b/src/Parser/Parser/Expression/ExpressionParser.php index 5f3829d2..15ff058c 100644 --- a/src/Parser/Parser/Expression/ExpressionParser.php +++ b/src/Parser/Parser/Expression/ExpressionParser.php @@ -40,7 +40,7 @@ use PackageFactory\ComponentEngine\Parser\Parser\UtilityParser; use Parsica\Parsica\Parser; -use function Parsica\Parsica\{any, between, char, either, pure, skipSpace}; +use function Parsica\Parsica\{any, char, either, pure}; final class ExpressionParser { diff --git a/src/Parser/Parser/Expression/ExpressionsParser.php b/src/Parser/Parser/Expression/ExpressionsParser.php new file mode 100644 index 00000000..3788a670 --- /dev/null +++ b/src/Parser/Parser/Expression/ExpressionsParser.php @@ -0,0 +1,47 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Parser\Parser\Expression; + +use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNodes; +use Parsica\Parsica\Parser; + +use function Parsica\Parsica\char; +use function Parsica\Parsica\sepBy1; + +final class ExpressionsParser +{ + private static ?Parser $instance = null; + + public static function get(): Parser + { + return self::$instance ??= self::build(); + } + + private static function build(): Parser + { + return sepBy1( + char(','), + ExpressionParser::get() + )->map(fn ($collected) => new ExpressionNodes(...$collected)); + } +} diff --git a/src/Parser/Parser/MatchArm/MatchArmParser.php b/src/Parser/Parser/MatchArm/MatchArmParser.php index 526d2a01..b623bff1 100644 --- a/src/Parser/Parser/MatchArm/MatchArmParser.php +++ b/src/Parser/Parser/MatchArm/MatchArmParser.php @@ -22,11 +22,10 @@ namespace PackageFactory\ComponentEngine\Parser\Parser\MatchArm; -use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNode; -use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNodes; use PackageFactory\ComponentEngine\Parser\Ast\MatchArmNode; use PackageFactory\ComponentEngine\Parser\Ast\MatchArmNodes; use PackageFactory\ComponentEngine\Parser\Parser\Expression\ExpressionParser; +use PackageFactory\ComponentEngine\Parser\Parser\Expression\ExpressionsParser; use Parsica\Parsica\Parser; use function Parsica\Parsica\collect; @@ -55,8 +54,8 @@ private static function getMatchArmParser(): Parser { return collect( either( - string('default')->map(fn () => null), - ExpressionParser::get()->map(fn (ExpressionNode $expressionNode) => new ExpressionNodes($expressionNode)) + string('default')->voidLeft(null), + ExpressionsParser::get() ), skipSpace(), string('->'), From 0b2326561094c0abcc5afda2d8f8d28e25390813 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 21 May 2023 13:14:59 +0200 Subject: [PATCH 18/20] WIP: Parsica clean up unnecessary flyweights (singletons) But cache ExpressionParser which speeds it up to 68ms --- src/Parser/Parser/Access/AccessParser.php | 1 + src/Parser/Parser/Attribute/AttributeParser.php | 10 ++-------- .../Parser/BooleanLiteral/BooleanLiteralParser.php | 12 +++--------- .../EnumMemberDeclarationParser.php | 2 +- src/Parser/Parser/Export/ExportParser.php | 11 +---------- src/Parser/Parser/Expression/ExpressionParser.php | 9 +++++++++ src/Parser/Parser/Expression/ExpressionsParser.php | 8 +------- src/Parser/Parser/Identifier/IdentifierParser.php | 10 +--------- src/Parser/Parser/MatchArm/MatchArmParser.php | 8 +------- src/Parser/Parser/NullLiteral/NullLiteralParser.php | 10 ++-------- .../PropertyDeclarationParser.php | 10 ++-------- .../Parser/StringLiteral/StringLiteralParser.php | 8 +------- src/Parser/Parser/TagContent/TagContentParser.php | 10 ++-------- .../Parser/TemplateLiteral/TemplateLiteralParser.php | 10 ++-------- .../Parser/TypeReference/TypeReferenceParser.php | 8 +------- .../Parser/UnaryOperation/UnaryOperationParser.php | 10 +--------- 16 files changed, 31 insertions(+), 106 deletions(-) diff --git a/src/Parser/Parser/Access/AccessParser.php b/src/Parser/Parser/Access/AccessParser.php index ed7f4a8e..9df0f168 100644 --- a/src/Parser/Parser/Access/AccessParser.php +++ b/src/Parser/Parser/Access/AccessParser.php @@ -38,6 +38,7 @@ final class AccessParser { + /** @return Parser */ public static function get(ExpressionNode $subject): Parser { return some( diff --git a/src/Parser/Parser/Attribute/AttributeParser.php b/src/Parser/Parser/Attribute/AttributeParser.php index 31f7ed81..66c3b319 100644 --- a/src/Parser/Parser/Attribute/AttributeParser.php +++ b/src/Parser/Parser/Attribute/AttributeParser.php @@ -38,14 +38,8 @@ final class AttributeParser { - private static ?Parser $instance = null; - + /** @return Parser */ public static function get(): Parser - { - return self::$instance ??= self::build(); - } - - private static function build(): Parser { return many( collect( @@ -61,7 +55,7 @@ private static function build(): Parser ) ) )->map(fn ($collected) => new AttributeNode($collected[1], $collected[3])) - )->map(fn ($collected) => new AttributeNodes(...$collected ? $collected : [])); + )->map(fn ($collected) => new AttributeNodes(...$collected ?? [])); } private static function attributeIdentifier(): Parser diff --git a/src/Parser/Parser/BooleanLiteral/BooleanLiteralParser.php b/src/Parser/Parser/BooleanLiteral/BooleanLiteralParser.php index af0292fc..474ac1e1 100644 --- a/src/Parser/Parser/BooleanLiteral/BooleanLiteralParser.php +++ b/src/Parser/Parser/BooleanLiteral/BooleanLiteralParser.php @@ -29,18 +29,12 @@ final class BooleanLiteralParser { - private static ?Parser $instance = null; - + /** @return Parser */ public static function get(): Parser - { - return self::$instance ??= self::build(); - } - - private static function build(): Parser { return skipSpace()->sequence(either( - string('true')->map(fn () => new BooleanLiteralNode(true)), - string('false')->map(fn () => new BooleanLiteralNode(false)) + string('true')->voidLeft(new BooleanLiteralNode(true)), + string('false')->voidLeft(new BooleanLiteralNode(false)) )); } } diff --git a/src/Parser/Parser/EnumMemberDeclaration/EnumMemberDeclarationParser.php b/src/Parser/Parser/EnumMemberDeclaration/EnumMemberDeclarationParser.php index 458a75d6..f37dcf53 100644 --- a/src/Parser/Parser/EnumMemberDeclaration/EnumMemberDeclarationParser.php +++ b/src/Parser/Parser/EnumMemberDeclaration/EnumMemberDeclarationParser.php @@ -69,6 +69,6 @@ private static function build(): Parser ), skipSpace() )->map(fn ($collected) => new EnumMemberDeclarationNode($collected[0], $collected[2])) - )->map(fn ($collected) => new EnumMemberDeclarationNodes(...$collected ? $collected : [])); + )->map(fn ($collected) => new EnumMemberDeclarationNodes(...$collected ?? [])); } } diff --git a/src/Parser/Parser/Export/ExportParser.php b/src/Parser/Parser/Export/ExportParser.php index c659bf4e..0c08aec2 100644 --- a/src/Parser/Parser/Export/ExportParser.php +++ b/src/Parser/Parser/Export/ExportParser.php @@ -23,8 +23,6 @@ namespace PackageFactory\ComponentEngine\Parser\Parser\Export; use PackageFactory\ComponentEngine\Parser\Ast\ExportNode; -use PackageFactory\ComponentEngine\Parser\Ast\ExportNodes; -use PackageFactory\ComponentEngine\Parser\Ast\StructDeclarationNode; use PackageFactory\ComponentEngine\Parser\Parser\ComponentDeclaration\ComponentDeclarationParser; use PackageFactory\ComponentEngine\Parser\Parser\EnumDeclaration\EnumDeclarationParser; use PackageFactory\ComponentEngine\Parser\Parser\StructDeclaration\StructDeclarationParser; @@ -33,19 +31,12 @@ use function Parsica\Parsica\any; use function Parsica\Parsica\collect; -use function Parsica\Parsica\many; use function Parsica\Parsica\skipSpace; final class ExportParser { - private static ?Parser $instance = null; - + /** @return Parser */ public static function get(): Parser - { - return self::$instance ??= self::build(); - } - - private static function build(): Parser { return collect( skipSpace(), diff --git a/src/Parser/Parser/Expression/ExpressionParser.php b/src/Parser/Parser/Expression/ExpressionParser.php index 15ff058c..c31b82da 100644 --- a/src/Parser/Parser/Expression/ExpressionParser.php +++ b/src/Parser/Parser/Expression/ExpressionParser.php @@ -44,6 +44,9 @@ final class ExpressionParser { + /** @var array> */ + private static $instances = []; + public static function parseFromString(string $string): ExpressionNode { return self::get()->thenEof()->tryString($string)->output(); @@ -51,6 +54,12 @@ public static function parseFromString(string $string): ExpressionNode /** @return Parser */ public static function get(Precedence $precedence = Precedence::SEQUENCE): Parser + { + return self::$instances[$precedence->value] ??= self::build($precedence); + } + + /** @return Parser */ + public static function build(Precedence $precedence = Precedence::SEQUENCE): Parser { $expressionRootParser = any( NumberLiteralParser::get(), diff --git a/src/Parser/Parser/Expression/ExpressionsParser.php b/src/Parser/Parser/Expression/ExpressionsParser.php index 3788a670..b414a25f 100644 --- a/src/Parser/Parser/Expression/ExpressionsParser.php +++ b/src/Parser/Parser/Expression/ExpressionsParser.php @@ -30,14 +30,8 @@ final class ExpressionsParser { - private static ?Parser $instance = null; - + /** @return Parser */ public static function get(): Parser - { - return self::$instance ??= self::build(); - } - - private static function build(): Parser { return sepBy1( char(','), diff --git a/src/Parser/Parser/Identifier/IdentifierParser.php b/src/Parser/Parser/Identifier/IdentifierParser.php index 72f6b658..651a836a 100644 --- a/src/Parser/Parser/Identifier/IdentifierParser.php +++ b/src/Parser/Parser/Identifier/IdentifierParser.php @@ -26,18 +26,10 @@ use PackageFactory\ComponentEngine\Parser\Parser\UtilityParser; use Parsica\Parsica\Parser; -use function Parsica\Parsica\{alphaChar, alphaNumChar, zeroOrMore}; - final class IdentifierParser { - private static ?Parser $instance = null; - + /** @return Parser */ public static function get(): Parser - { - return self::$instance ??= self::build(); - } - - private static function build(): Parser { return UtilityParser::identifier() ->map(fn ($name) => new IdentifierNode($name)); diff --git a/src/Parser/Parser/MatchArm/MatchArmParser.php b/src/Parser/Parser/MatchArm/MatchArmParser.php index b623bff1..6d5de4d1 100644 --- a/src/Parser/Parser/MatchArm/MatchArmParser.php +++ b/src/Parser/Parser/MatchArm/MatchArmParser.php @@ -36,14 +36,8 @@ final class MatchArmParser { - private static ?Parser $instance = null; - + /** @return Parser */ public static function get(): Parser - { - return self::$instance ??= self::build(); - } - - private static function build(): Parser { return many( self::getMatchArmParser() diff --git a/src/Parser/Parser/NullLiteral/NullLiteralParser.php b/src/Parser/Parser/NullLiteral/NullLiteralParser.php index 6a40cbee..c6a17926 100644 --- a/src/Parser/Parser/NullLiteral/NullLiteralParser.php +++ b/src/Parser/Parser/NullLiteral/NullLiteralParser.php @@ -29,15 +29,9 @@ final class NullLiteralParser { - private static ?Parser $instance = null; - + /** @return Parser */ public static function get(): Parser { - return self::$instance ??= self::build(); - } - - private static function build(): Parser - { - return string('null')->map(fn () => new NullLiteralNode()); + return string('null')->voidLeft(new NullLiteralNode()); } } diff --git a/src/Parser/Parser/PropertyDeclaration/PropertyDeclarationParser.php b/src/Parser/Parser/PropertyDeclaration/PropertyDeclarationParser.php index c485c1f7..8d5af3de 100644 --- a/src/Parser/Parser/PropertyDeclaration/PropertyDeclarationParser.php +++ b/src/Parser/Parser/PropertyDeclaration/PropertyDeclarationParser.php @@ -35,14 +35,8 @@ final class PropertyDeclarationParser { - private static ?Parser $instance = null; - + /** @return Parser */ public static function get(): Parser - { - return self::$instance ??= self::build(); - } - - private static function build(): Parser { return many( collect( @@ -53,6 +47,6 @@ private static function build(): Parser TypeReferenceParser::get(), skipSpace() )->map(fn ($collected) => new PropertyDeclarationNode($collected[0], $collected[4])) - )->map(fn ($collected) => new PropertyDeclarationNodes(...$collected ? $collected : [])); + )->map(fn ($collected) => new PropertyDeclarationNodes(...$collected ?? [])); } } diff --git a/src/Parser/Parser/StringLiteral/StringLiteralParser.php b/src/Parser/Parser/StringLiteral/StringLiteralParser.php index ad954b1a..b2a2f41f 100644 --- a/src/Parser/Parser/StringLiteral/StringLiteralParser.php +++ b/src/Parser/Parser/StringLiteral/StringLiteralParser.php @@ -28,14 +28,8 @@ final class StringLiteralParser { - private static ?Parser $instance = null; - + /** @return Parser */ public static function get(): Parser - { - return self::$instance ??= self::build(); - } - - private static function build(): Parser { return UtilityParser::quotedStringContents()->map(fn (string $contents) => new StringLiteralNode($contents)); } diff --git a/src/Parser/Parser/TagContent/TagContentParser.php b/src/Parser/Parser/TagContent/TagContentParser.php index 53770b49..34f55a68 100644 --- a/src/Parser/Parser/TagContent/TagContentParser.php +++ b/src/Parser/Parser/TagContent/TagContentParser.php @@ -36,19 +36,13 @@ final class TagContentParser { - private static ?Parser $instance = null; - + /** @return Parser */ public static function get(): Parser - { - return self::$instance ??= self::build(); - } - - private static function build(): Parser { return skipSpace()->sequence( many( self::tagContent() - )->map(fn ($collected) => new TagContentNodes(...$collected ? array_filter($collected) : [])) + )->map(fn ($collected) => new TagContentNodes(...array_filter($collected ?? []))) ); } diff --git a/src/Parser/Parser/TemplateLiteral/TemplateLiteralParser.php b/src/Parser/Parser/TemplateLiteral/TemplateLiteralParser.php index 709c354d..7d5d5d0d 100644 --- a/src/Parser/Parser/TemplateLiteral/TemplateLiteralParser.php +++ b/src/Parser/Parser/TemplateLiteral/TemplateLiteralParser.php @@ -35,14 +35,8 @@ final class TemplateLiteralParser { - private static ?Parser $instance = null; - + /** @return Parser */ public static function get(): Parser - { - return self::$instance ??= self::build(); - } - - private static function build(): Parser { return char('`')->bind( fn () => zeroOrMore( @@ -50,7 +44,7 @@ private static function build(): Parser self::stringLiteral(), self::expression(), )->map(fn ($item) => [$item]) - )->map(fn ($collected) => new TemplateLiteralNode(...$collected ? $collected : [])) + )->map(fn ($collected) => new TemplateLiteralNode(...$collected ?? [])) ->thenIgnore(char('`')) ); } diff --git a/src/Parser/Parser/TypeReference/TypeReferenceParser.php b/src/Parser/Parser/TypeReference/TypeReferenceParser.php index c9a92571..532d78b9 100644 --- a/src/Parser/Parser/TypeReference/TypeReferenceParser.php +++ b/src/Parser/Parser/TypeReference/TypeReferenceParser.php @@ -35,14 +35,8 @@ final class TypeReferenceParser { - private static ?Parser $instance = null; - + /** @return Parser */ public static function get(): Parser - { - return self::$instance ??= self::build(); - } - - private static function build(): Parser { return collect( optional(char('?')), diff --git a/src/Parser/Parser/UnaryOperation/UnaryOperationParser.php b/src/Parser/Parser/UnaryOperation/UnaryOperationParser.php index 3e29f746..21a56704 100644 --- a/src/Parser/Parser/UnaryOperation/UnaryOperationParser.php +++ b/src/Parser/Parser/UnaryOperation/UnaryOperationParser.php @@ -22,25 +22,17 @@ namespace PackageFactory\ComponentEngine\Parser\Parser\UnaryOperation; -use PackageFactory\ComponentEngine\Definition\Precedence; use PackageFactory\ComponentEngine\Definition\UnaryOperator; use PackageFactory\ComponentEngine\Parser\Ast\UnaryOperationNode; use PackageFactory\ComponentEngine\Parser\Parser\Expression\ExpressionParser; use Parsica\Parsica\Parser; use function Parsica\Parsica\char; -use function Parsica\Parsica\collect; final class UnaryOperationParser { - private static ?Parser $instance = null; - + /** @return Parser */ public static function get(): Parser - { - return self::$instance ??= self::build(); - } - - private static function build(): Parser { return self::unaryOperator()->bind(function (UnaryOperator $unaryOperator) { return ExpressionParser::get($unaryOperator->toPrecedence())->map(fn ($expression) => new UnaryOperationNode($unaryOperator, $expression)); From b579257b7da045bb405491bd2c2dd123d66295a2 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Mon, 29 May 2023 11:13:16 +0200 Subject: [PATCH 19/20] TASK: ExpressionParser use lazy identity for nested parsers this is to avoid infinite recursion, in case the nested parser calls `ExpressionParser::get()` --- .../Parser/Expression/ExpressionParser.php | 29 ++++++++++++------- src/Parser/Parser/Match/MatchParser.php | 27 +++++++++-------- src/Parser/Parser/Tag/TagParser.php | 2 +- .../TemplateLiteral/TemplateLiteralParser.php | 4 +-- 4 files changed, 35 insertions(+), 27 deletions(-) diff --git a/src/Parser/Parser/Expression/ExpressionParser.php b/src/Parser/Parser/Expression/ExpressionParser.php index c31b82da..9bd2cfc0 100644 --- a/src/Parser/Parser/Expression/ExpressionParser.php +++ b/src/Parser/Parser/Expression/ExpressionParser.php @@ -40,7 +40,7 @@ use PackageFactory\ComponentEngine\Parser\Parser\UtilityParser; use Parsica\Parsica\Parser; -use function Parsica\Parsica\{any, char, either, pure}; +use function Parsica\Parsica\{any, char, either, pure, succeed}; final class ExpressionParser { @@ -61,16 +61,25 @@ public static function get(Precedence $precedence = Precedence::SEQUENCE): Parse /** @return Parser */ public static function build(Precedence $precedence = Precedence::SEQUENCE): Parser { + /** + * Lazy identity, to avoid infinite recursion, in case the nested parser calls `ExpressionParser::get()` + * + * @template T + * @param callable(): Parser $f + * @return Parser + */ + $lazy = fn (callable|array $f): Parser => succeed()->bind($f); + $expressionRootParser = any( - NumberLiteralParser::get(), - BooleanLiteralParser::get(), - NullLiteralParser::get(), - MatchParser::get(), - TagParser::get(), - StringLiteralParser::get(), - IdentifierParser::get(), - TemplateLiteralParser::get(), - UnaryOperationParser::get() + $lazy(fn () => NumberLiteralParser::get()), + $lazy(fn () => BooleanLiteralParser::get()), + $lazy(fn () => NullLiteralParser::get()), + $lazy(fn () => MatchParser::get()), + $lazy(fn () => TagParser::get()), + $lazy(fn () => StringLiteralParser::get()), + $lazy(fn () => IdentifierParser::get()), + $lazy(fn () => TemplateLiteralParser::get()), + $lazy(fn () => UnaryOperationParser::get()) ); return UtilityParser::skipSpaceAndComments()->sequence( diff --git a/src/Parser/Parser/Match/MatchParser.php b/src/Parser/Parser/Match/MatchParser.php index 1ec2dbf9..04d93ae5 100644 --- a/src/Parser/Parser/Match/MatchParser.php +++ b/src/Parser/Parser/Match/MatchParser.php @@ -43,20 +43,19 @@ public static function get(): Parser private static function build(): Parser { - // @todo for some reason we must use bind here to avoid infinite recursion - return string('match') - ->bind(fn () => collect( - skipSpace(), - char('('), - ExpressionParser::get(), - char(')'), - skipSpace(), - char('{'), - skipSpace(), - MatchArmParser::get(), - skipSpace(), - char('}') - )->map(fn ($collected) => new MatchNode($collected[2], $collected[7])) + return collect( + string('match'), + skipSpace(), + char('('), + ExpressionParser::get(), + char(')'), + skipSpace(), + char('{'), + skipSpace(), + MatchArmParser::get(), + skipSpace(), + char('}') + )->map(fn ($collected) => new MatchNode($collected[3], $collected[8]) ); } } diff --git a/src/Parser/Parser/Tag/TagParser.php b/src/Parser/Parser/Tag/TagParser.php index 6488798a..d642e053 100644 --- a/src/Parser/Parser/Tag/TagParser.php +++ b/src/Parser/Parser/Tag/TagParser.php @@ -49,7 +49,7 @@ public static function get(): Parser } private static function build(): Parser { - return char('<')->bind(fn () => + return char('<')->sequence( self::tagName()->bind(fn (string $tagName) => skipSpace()->followedBy(AttributeParser::get())->thenIgnore(skipSpace())->bind(fn ($attributeNodes) => either( diff --git a/src/Parser/Parser/TemplateLiteral/TemplateLiteralParser.php b/src/Parser/Parser/TemplateLiteral/TemplateLiteralParser.php index 7d5d5d0d..82dd515f 100644 --- a/src/Parser/Parser/TemplateLiteral/TemplateLiteralParser.php +++ b/src/Parser/Parser/TemplateLiteral/TemplateLiteralParser.php @@ -38,8 +38,8 @@ final class TemplateLiteralParser /** @return Parser */ public static function get(): Parser { - return char('`')->bind( - fn () => zeroOrMore( + return char('`')->sequence( + zeroOrMore( any( self::stringLiteral(), self::expression(), From 0fa187a704e7227d27526cb70e87cb5a7d0a44a9 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Mon, 29 May 2023 11:54:12 +0200 Subject: [PATCH 20/20] TASK: Add instance cache for all expressionRootParser partially reverts: 0b2326561094c0abcc5afda2d8f8d28e25390813 --- .../BooleanLiteral/BooleanLiteralParser.php | 9 +++++++-- .../ComponentDeclarationParser.php | 16 +++------------- .../EnumDeclaration/EnumDeclarationParser.php | 12 +++++------- .../EnumMemberDeclarationParser.php | 13 +------------ .../Parser/Identifier/IdentifierParser.php | 5 ++++- src/Parser/Parser/Match/MatchParser.php | 11 ++++------- src/Parser/Parser/Module/ModuleParser.php | 6 ++++-- .../Parser/NullLiteral/NullLiteralParser.php | 7 +++++-- .../Parser/NumberLiteral/NumberLiteralParser.php | 9 ++++++--- .../Parser/StringLiteral/StringLiteralParser.php | 6 +++++- .../StructDeclarationParser.php | 11 ++++------- src/Parser/Parser/Tag/TagParser.php | 10 ++++------ .../TemplateLiteral/TemplateLiteralParser.php | 5 ++++- src/Parser/Parser/Text/TextParser.php | 3 ++- .../UnaryOperation/UnaryOperationParser.php | 5 ++++- 15 files changed, 62 insertions(+), 66 deletions(-) diff --git a/src/Parser/Parser/BooleanLiteral/BooleanLiteralParser.php b/src/Parser/Parser/BooleanLiteral/BooleanLiteralParser.php index 474ac1e1..f57672d4 100644 --- a/src/Parser/Parser/BooleanLiteral/BooleanLiteralParser.php +++ b/src/Parser/Parser/BooleanLiteral/BooleanLiteralParser.php @@ -25,14 +25,19 @@ use PackageFactory\ComponentEngine\Parser\Ast\BooleanLiteralNode; use Parsica\Parsica\Parser; -use function Parsica\Parsica\{either, skipSpace, string}; +use function Parsica\Parsica\either; +use function Parsica\Parsica\skipSpace; +use function Parsica\Parsica\string; final class BooleanLiteralParser { + /** @var Parser */ + private static Parser $i; + /** @return Parser */ public static function get(): Parser { - return skipSpace()->sequence(either( + return self::$i ??= skipSpace()->sequence(either( string('true')->voidLeft(new BooleanLiteralNode(true)), string('false')->voidLeft(new BooleanLiteralNode(false)) )); diff --git a/src/Parser/Parser/ComponentDeclaration/ComponentDeclarationParser.php b/src/Parser/Parser/ComponentDeclaration/ComponentDeclarationParser.php index 3458af0a..649e6a45 100644 --- a/src/Parser/Parser/ComponentDeclaration/ComponentDeclarationParser.php +++ b/src/Parser/Parser/ComponentDeclaration/ComponentDeclarationParser.php @@ -24,7 +24,6 @@ use PackageFactory\ComponentEngine\Parser\Ast\ComponentDeclarationNode; use PackageFactory\ComponentEngine\Parser\Parser\Expression\ExpressionParser; -use PackageFactory\ComponentEngine\Parser\Parser\ParseFromString; use PackageFactory\ComponentEngine\Parser\Parser\PropertyDeclaration\PropertyDeclarationParser; use PackageFactory\ComponentEngine\Parser\Parser\UtilityParser; use Parsica\Parsica\Parser; @@ -33,13 +32,10 @@ use function Parsica\Parsica\collect; use function Parsica\Parsica\skipSpace; -/** - * @template T - */ final class ComponentDeclarationParser { - /** @var ?Parser */ - private static ?Parser $instance = null; + /** @var Parser */ + private static Parser $i; public static function parseFromString(string $string): ComponentDeclarationNode { @@ -49,13 +45,7 @@ public static function parseFromString(string $string): ComponentDeclarationNode /** @return Parser */ public static function get(): Parser { - return self::$instance ??= self::build(); - } - - /** @return Parser */ - private static function build(): Parser - { - return collect( + return self::$i ??= collect( UtilityParser::keyword('component'), skipSpace(), UtilityParser::identifier(), diff --git a/src/Parser/Parser/EnumDeclaration/EnumDeclarationParser.php b/src/Parser/Parser/EnumDeclaration/EnumDeclarationParser.php index 3864d469..9c836cb1 100644 --- a/src/Parser/Parser/EnumDeclaration/EnumDeclarationParser.php +++ b/src/Parser/Parser/EnumDeclaration/EnumDeclarationParser.php @@ -33,21 +33,19 @@ final class EnumDeclarationParser { - private static ?Parser $instance = null; + /** @var Parser */ + private static Parser $i; + /** @return Parser */ public static function parseFromString(string $string): EnumDeclarationNode { return self::get()->thenEof()->tryString($string)->output(); } + /** @return Parser */ public static function get(): Parser { - return self::$instance ??= self::build(); - } - - private static function build(): Parser - { - return collect( + return self::$i ??= collect( UtilityParser::keyword('enum'), skipSpace(), UtilityParser::identifier(), diff --git a/src/Parser/Parser/EnumMemberDeclaration/EnumMemberDeclarationParser.php b/src/Parser/Parser/EnumMemberDeclaration/EnumMemberDeclarationParser.php index f37dcf53..ca648040 100644 --- a/src/Parser/Parser/EnumMemberDeclaration/EnumMemberDeclarationParser.php +++ b/src/Parser/Parser/EnumMemberDeclaration/EnumMemberDeclarationParser.php @@ -22,18 +22,13 @@ namespace PackageFactory\ComponentEngine\Parser\Parser\EnumMemberDeclaration; -use PackageFactory\ComponentEngine\Parser\Ast\EnumDeclarationNode; use PackageFactory\ComponentEngine\Parser\Ast\EnumMemberDeclarationNode; use PackageFactory\ComponentEngine\Parser\Ast\EnumMemberDeclarationNodes; -use PackageFactory\ComponentEngine\Parser\Ast\PropertyDeclarationNode; -use PackageFactory\ComponentEngine\Parser\Ast\PropertyDeclarationNodes; use PackageFactory\ComponentEngine\Parser\Parser\NumberLiteral\NumberLiteralParser; use PackageFactory\ComponentEngine\Parser\Parser\StringLiteral\StringLiteralParser; -use PackageFactory\ComponentEngine\Parser\Parser\TypeReference\TypeReferenceParser; use PackageFactory\ComponentEngine\Parser\Parser\UtilityParser; use Parsica\Parsica\Parser; -use function Parsica\Parsica\any; use function Parsica\Parsica\between; use function Parsica\Parsica\char; use function Parsica\Parsica\collect; @@ -44,14 +39,8 @@ final class EnumMemberDeclarationParser { - private static ?Parser $instance = null; - + /** @return Parser */ public static function get(): Parser - { - return self::$instance ??= self::build(); - } - - private static function build(): Parser { return many( collect( diff --git a/src/Parser/Parser/Identifier/IdentifierParser.php b/src/Parser/Parser/Identifier/IdentifierParser.php index 651a836a..79b1f3ee 100644 --- a/src/Parser/Parser/Identifier/IdentifierParser.php +++ b/src/Parser/Parser/Identifier/IdentifierParser.php @@ -28,10 +28,13 @@ final class IdentifierParser { + /** @var Parser */ + private static Parser $i; + /** @return Parser */ public static function get(): Parser { - return UtilityParser::identifier() + return self::$i ??= UtilityParser::identifier() ->map(fn ($name) => new IdentifierNode($name)); } } diff --git a/src/Parser/Parser/Match/MatchParser.php b/src/Parser/Parser/Match/MatchParser.php index 04d93ae5..9a15c3f5 100644 --- a/src/Parser/Parser/Match/MatchParser.php +++ b/src/Parser/Parser/Match/MatchParser.php @@ -34,16 +34,13 @@ final class MatchParser { - private static ?Parser $instance = null; + /** @var Parser */ + private static Parser $i; + /** @return Parser */ public static function get(): Parser { - return self::$instance ??= self::build(); - } - - private static function build(): Parser - { - return collect( + return self::$i ??= collect( string('match'), skipSpace(), char('('), diff --git a/src/Parser/Parser/Module/ModuleParser.php b/src/Parser/Parser/Module/ModuleParser.php index 425a50bc..c9f3f5b0 100644 --- a/src/Parser/Parser/Module/ModuleParser.php +++ b/src/Parser/Parser/Module/ModuleParser.php @@ -37,7 +37,8 @@ final class ModuleParser { - private static ?Parser $instance = null; + /** @var Parser */ + private static Parser $i; public static function parseFromStream(Stream $stream): ModuleNode { @@ -49,9 +50,10 @@ public static function parseFromString(string $string): ModuleNode return self::get()->thenEof()->tryString($string)->output(); } + /** @return Parser */ public static function get(): Parser { - return self::$instance ??= UtilityParser::skipSpaceAndComments()->then(many( + return self::$i ??= UtilityParser::skipSpaceAndComments()->then(many( either( ImportParser::get(), ExportParser::get() diff --git a/src/Parser/Parser/NullLiteral/NullLiteralParser.php b/src/Parser/Parser/NullLiteral/NullLiteralParser.php index c6a17926..5c392197 100644 --- a/src/Parser/Parser/NullLiteral/NullLiteralParser.php +++ b/src/Parser/Parser/NullLiteral/NullLiteralParser.php @@ -25,13 +25,16 @@ use PackageFactory\ComponentEngine\Parser\Ast\NullLiteralNode; use Parsica\Parsica\Parser; -use function Parsica\Parsica\{string}; +use function Parsica\Parsica\string; final class NullLiteralParser { + /** @var Parser */ + private static Parser $i; + /** @return Parser */ public static function get(): Parser { - return string('null')->voidLeft(new NullLiteralNode()); + return self::$i ??= string('null')->voidLeft(new NullLiteralNode()); } } diff --git a/src/Parser/Parser/NumberLiteral/NumberLiteralParser.php b/src/Parser/Parser/NumberLiteral/NumberLiteralParser.php index d45556ba..80e1617a 100644 --- a/src/Parser/Parser/NumberLiteral/NumberLiteralParser.php +++ b/src/Parser/Parser/NumberLiteral/NumberLiteralParser.php @@ -43,14 +43,17 @@ final class NumberLiteralParser { - private static ?Parser $instance = null; + /** @var Parser */ + private static Parser $i; + /** @return Parser */ public static function get(): Parser { - return self::$instance ??= self::build(); + return self::$i ??= self::initialize(); } - private static function build(): Parser + /** @return Parser */ + private static function initialize(): Parser { $isOctalDigit = isCharCode(range(0x30, 0x37)); $isBinaryDigit = isCharCode([0x30, 0x31]); diff --git a/src/Parser/Parser/StringLiteral/StringLiteralParser.php b/src/Parser/Parser/StringLiteral/StringLiteralParser.php index b2a2f41f..c272c91d 100644 --- a/src/Parser/Parser/StringLiteral/StringLiteralParser.php +++ b/src/Parser/Parser/StringLiteral/StringLiteralParser.php @@ -28,9 +28,13 @@ final class StringLiteralParser { + /** @var Parser */ + private static Parser $i; + /** @return Parser */ public static function get(): Parser { - return UtilityParser::quotedStringContents()->map(fn (string $contents) => new StringLiteralNode($contents)); + return self::$i ??= UtilityParser::quotedStringContents() + ->map(fn (string $contents) => new StringLiteralNode($contents)); } } diff --git a/src/Parser/Parser/StructDeclaration/StructDeclarationParser.php b/src/Parser/Parser/StructDeclaration/StructDeclarationParser.php index 4e2fb33d..626408d2 100644 --- a/src/Parser/Parser/StructDeclaration/StructDeclarationParser.php +++ b/src/Parser/Parser/StructDeclaration/StructDeclarationParser.php @@ -33,21 +33,18 @@ final class StructDeclarationParser { - private static ?Parser $instance = null; + /** @var Parser */ + private static Parser $i; public static function parseFromString(string $string): StructDeclarationNode { return self::get()->thenEof()->tryString($string)->output(); } + /** @return Parser */ public static function get(): Parser { - return self::$instance ??= self::build(); - } - - private static function build(): Parser - { - return collect( + return self::$i ??= collect( UtilityParser::keyword('struct'), skipSpace(), UtilityParser::identifier(), diff --git a/src/Parser/Parser/Tag/TagParser.php b/src/Parser/Parser/Tag/TagParser.php index d642e053..f36b1535 100644 --- a/src/Parser/Parser/Tag/TagParser.php +++ b/src/Parser/Parser/Tag/TagParser.php @@ -41,15 +41,13 @@ final class TagParser { - private static ?Parser $instance = null; + /** @var Parser */ + private static Parser $i; + /** @return Parser */ public static function get(): Parser { - return self::$instance ??= self::build(); - } - private static function build(): Parser - { - return char('<')->sequence( + return self::$i ??= char('<')->sequence( self::tagName()->bind(fn (string $tagName) => skipSpace()->followedBy(AttributeParser::get())->thenIgnore(skipSpace())->bind(fn ($attributeNodes) => either( diff --git a/src/Parser/Parser/TemplateLiteral/TemplateLiteralParser.php b/src/Parser/Parser/TemplateLiteral/TemplateLiteralParser.php index 82dd515f..077c97a4 100644 --- a/src/Parser/Parser/TemplateLiteral/TemplateLiteralParser.php +++ b/src/Parser/Parser/TemplateLiteral/TemplateLiteralParser.php @@ -35,10 +35,13 @@ final class TemplateLiteralParser { + /** @var Parser */ + private static Parser $i; + /** @return Parser */ public static function get(): Parser { - return char('`')->sequence( + return self::$i ??= char('`')->sequence( zeroOrMore( any( self::stringLiteral(), diff --git a/src/Parser/Parser/Text/TextParser.php b/src/Parser/Parser/Text/TextParser.php index 9f5f0e20..3b265fe9 100644 --- a/src/Parser/Parser/Text/TextParser.php +++ b/src/Parser/Parser/Text/TextParser.php @@ -32,6 +32,7 @@ final class TextParser { + /** @return Parser */ public static function get(): Parser { return @@ -51,7 +52,7 @@ public static function get(): Parser if (!$trimmedNewlinesAndSpaces || ctype_space($trimmedNewlinesAndSpaces)) { return null; } - return new TextNode($trimmedNewlinesAndSpaces); + return new TextNode($trimmedNewlinesAndSpaces); }); } } diff --git a/src/Parser/Parser/UnaryOperation/UnaryOperationParser.php b/src/Parser/Parser/UnaryOperation/UnaryOperationParser.php index 21a56704..581fc9f4 100644 --- a/src/Parser/Parser/UnaryOperation/UnaryOperationParser.php +++ b/src/Parser/Parser/UnaryOperation/UnaryOperationParser.php @@ -31,10 +31,13 @@ final class UnaryOperationParser { + /** @var Parser */ + private static Parser $i; + /** @return Parser */ public static function get(): Parser { - return self::unaryOperator()->bind(function (UnaryOperator $unaryOperator) { + return self::$i ??= self::unaryOperator()->bind(function (UnaryOperator $unaryOperator) { return ExpressionParser::get($unaryOperator->toPrecedence())->map(fn ($expression) => new UnaryOperationNode($unaryOperator, $expression)); }); }