diff --git a/composer.json b/composer.json index 69461558..3565984a 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,8 @@ "test": "./scripts/test" }, "require": { - "php": ">=8.1" + "php": ">=8.1", + "parsica-php/parsica": "^0.8.1" }, "autoload": { "psr-4": { diff --git a/src/Definition/Precedence.php b/src/Definition/Precedence.php index 49da1b56..5051649b 100644 --- a/src/Definition/Precedence.php +++ b/src/Definition/Precedence.php @@ -24,8 +24,6 @@ use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; - - enum Precedence: int { // @@ -81,8 +79,13 @@ public static function forTokenType(TokenType $tokenType): self }; } - public function mustStopAt(TokenType $tokenType): bool + 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/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/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/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/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/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/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/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 673bcd73..3ecc46b7 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/ExpressionNode.php b/src/Parser/Ast/ExpressionNode.php index aa26e8c0..7a22f496 100644 --- a/src/Parser/Ast/ExpressionNode.php +++ b/src/Parser/Ast/ExpressionNode.php @@ -23,27 +23,23 @@ 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 ) { } + /** @deprecated */ public static function fromString(string $expressionAsString): self { - return self::fromTokens( - Tokenizer::fromSource( - Source::fromString($expressionAsString) - )->getIterator() - ); + return ExpressionParser::parseFromString($expressionAsString); } /** @@ -123,7 +119,7 @@ public static function fromTokens(\Iterator $tokens, Precedence $precedence = Pr Scanner::skipSpaceAndComments($tokens); - while (!Scanner::isEnd($tokens) && !$precedence->mustStopAt(Scanner::type($tokens))) { + while (!Scanner::isEnd($tokens) && !$precedence->mustStopAtTokenType(Scanner::type($tokens))) { switch (Scanner::type($tokens)) { case TokenType::OPERATOR_BOOLEAN_AND: case TokenType::OPERATOR_BOOLEAN_OR: 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/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/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..f3cca333 100644 --- a/src/Parser/Ast/ImportNodes.php +++ b/src/Parser/Ast/ImportNodes.php @@ -29,18 +29,29 @@ 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 + { + // 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 @@ -51,7 +62,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/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/Ast/ModuleNode.php b/src/Parser/Ast/ModuleNode.php index e49b03f9..bff83a10 100644 --- a/src/Parser/Ast/ModuleNode.php +++ b/src/Parser/Ast/ModuleNode.php @@ -22,15 +22,14 @@ namespace PackageFactory\ComponentEngine\Parser\Ast; -use PackageFactory\ComponentEngine\Parser\Source\Source; -use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; +use PackageFactory\ComponentEngine\Parser\Parser\Module\ModuleParser; 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 { - private function __construct( + public function __construct( public readonly ImportNodes $imports, public readonly ExportNodes $exports, ) { @@ -38,11 +37,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/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/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/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/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/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/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/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/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/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/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..9df0f168 --- /dev/null +++ b/src/Parser/Parser/Access/AccessParser.php @@ -0,0 +1,62 @@ +. + */ + +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\char; +use function Parsica\Parsica\collect; +use function Parsica\Parsica\either; +use function Parsica\Parsica\some; +use function Parsica\Parsica\string; + +final class AccessParser +{ + /** @return Parser */ + public static function get(ExpressionNode $subject): Parser + { + return some( + 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/Attribute/AttributeParser.php b/src/Parser/Parser/Attribute/AttributeParser.php new file mode 100644 index 00000000..66c3b319 --- /dev/null +++ b/src/Parser/Parser/Attribute/AttributeParser.php @@ -0,0 +1,65 @@ +. + */ + +declare(strict_types=1); + +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\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 +{ + /** @return Parser */ + public static function get(): Parser + { + 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 ?? [])); + } + + private static function attributeIdentifier(): Parser + { + return UtilityParser::identifier(); + } +} diff --git a/src/Parser/Parser/BinaryOperation/BinaryOperationParser.php b/src/Parser/Parser/BinaryOperation/BinaryOperationParser.php new file mode 100644 index 00000000..d5d3d046 --- /dev/null +++ b/src/Parser/Parser/BinaryOperation/BinaryOperationParser.php @@ -0,0 +1,70 @@ +. + */ + +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 self::binaryOperatorParser()->bind(function (BinaryOperator $binaryOperator) use ($left) { + return ExpressionParser::get($binaryOperator->toPrecedence())->map(fn ($right) => new BinaryOperationNode( + $left, + $binaryOperator, + $right + )); + }); + } + + 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..f57672d4 --- /dev/null +++ b/src/Parser/Parser/BooleanLiteral/BooleanLiteralParser.php @@ -0,0 +1,45 @@ +. + */ + +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; +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 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 new file mode 100644 index 00000000..649e6a45 --- /dev/null +++ b/src/Parser/Parser/ComponentDeclaration/ComponentDeclarationParser.php @@ -0,0 +1,68 @@ +. + */ + +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\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 ComponentDeclarationParser +{ + /** @var Parser */ + private static Parser $i; + + public static function parseFromString(string $string): ComponentDeclarationNode + { + return self::get()->thenEof()->tryString($string)->output(); + } + + /** @return Parser */ + public static function get(): Parser + { + return self::$i ??= 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..9c836cb1 --- /dev/null +++ b/src/Parser/Parser/EnumDeclaration/EnumDeclarationParser.php @@ -0,0 +1,64 @@ +. + */ + +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 +{ + /** @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::$i ??= 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..ca648040 --- /dev/null +++ b/src/Parser/Parser/EnumMemberDeclaration/EnumMemberDeclarationParser.php @@ -0,0 +1,63 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Parser\Parser\EnumMemberDeclaration; + +use PackageFactory\ComponentEngine\Parser\Ast\EnumMemberDeclarationNode; +use PackageFactory\ComponentEngine\Parser\Ast\EnumMemberDeclarationNodes; +use PackageFactory\ComponentEngine\Parser\Parser\NumberLiteral\NumberLiteralParser; +use PackageFactory\ComponentEngine\Parser\Parser\StringLiteral\StringLiteralParser; +use PackageFactory\ComponentEngine\Parser\Parser\UtilityParser; +use Parsica\Parsica\Parser; + +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 +{ + /** @return Parser */ + public static function get(): 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 ?? [])); + } +} diff --git a/src/Parser/Parser/Export/ExportParser.php b/src/Parser/Parser/Export/ExportParser.php new file mode 100644 index 00000000..0c08aec2 --- /dev/null +++ b/src/Parser/Parser/Export/ExportParser.php @@ -0,0 +1,53 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Parser\Parser\Export; + +use PackageFactory\ComponentEngine\Parser\Ast\ExportNode; +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\skipSpace; + +final class ExportParser +{ + /** @return Parser */ + public static function get(): 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 new file mode 100644 index 00000000..9bd2cfc0 --- /dev/null +++ b/src/Parser/Parser/Expression/ExpressionParser.php @@ -0,0 +1,115 @@ +. + */ + +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\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\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, char, either, pure, succeed}; + +final class ExpressionParser +{ + /** @var array> */ + private static $instances = []; + + public static function parseFromString(string $string): ExpressionNode + { + return self::get()->thenEof()->tryString($string)->output(); + } + + /** @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 + { + /** + * 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( + $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( + either( + char('(')->bind( + fn () => self::get()->thenIgnore(char(')'))->thenIgnore(UtilityParser::skipSpaceAndComments()) + ), + $expressionRootParser->thenIgnore(UtilityParser::skipSpaceAndComments())->bind( + fn ($expressionRoot) => self::continueParsingWhilePrecedence(new ExpressionNode($expressionRoot), $precedence) + ), + ) + ); + } + + /** @return Parser */ + private static function continueParsingWhilePrecedence(ExpressionNode $expressionNode, Precedence $precedence): Parser + { + $continuationParser = any( + BinaryOperationParser::get($expressionNode), + AccessParser::get($expressionNode), + TernaryOperationParser::get($expressionNode) + ) + ->thenIgnore(UtilityParser::skipSpaceAndComments()) + ->bind( + fn ($expressionRoot) => self::continueParsingWhilePrecedence(new ExpressionNode($expressionRoot), $precedence) + ); + + return either( + PrecedenceParser::hasPrecedence($precedence)->sequence($continuationParser), + pure($expressionNode) + ); + } +} diff --git a/src/Parser/Parser/Expression/ExpressionsParser.php b/src/Parser/Parser/Expression/ExpressionsParser.php new file mode 100644 index 00000000..b414a25f --- /dev/null +++ b/src/Parser/Parser/Expression/ExpressionsParser.php @@ -0,0 +1,41 @@ +. + */ + +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 +{ + /** @return Parser */ + public static function get(): Parser + { + return sepBy1( + char(','), + ExpressionParser::get() + )->map(fn ($collected) => new ExpressionNodes(...$collected)); + } +} diff --git a/src/Parser/Parser/Identifier/IdentifierParser.php b/src/Parser/Parser/Identifier/IdentifierParser.php new file mode 100644 index 00000000..79b1f3ee --- /dev/null +++ b/src/Parser/Parser/Identifier/IdentifierParser.php @@ -0,0 +1,40 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Parser\Parser\Identifier; + +use PackageFactory\ComponentEngine\Parser\Ast\IdentifierNode; +use PackageFactory\ComponentEngine\Parser\Parser\UtilityParser; +use Parsica\Parsica\Parser; + +final class IdentifierParser +{ + /** @var Parser */ + private static Parser $i; + + /** @return Parser */ + public static function get(): Parser + { + return self::$i ??= UtilityParser::identifier() + ->map(fn ($name) => new IdentifierNode($name)); + } +} diff --git a/src/Parser/Parser/Import/ImportParser.php b/src/Parser/Parser/Import/ImportParser.php new file mode 100644 index 00000000..08d3ad3f --- /dev/null +++ b/src/Parser/Parser/Import/ImportParser.php @@ -0,0 +1,75 @@ +. + */ + +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\sepBy1; +use function Parsica\Parsica\skipSpace; + +final class ImportParser +{ + /** @return Parser */ + public static function get(): Parser + { + 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(), + ), + 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/Match/MatchParser.php b/src/Parser/Parser/Match/MatchParser.php new file mode 100644 index 00000000..9a15c3f5 --- /dev/null +++ b/src/Parser/Parser/Match/MatchParser.php @@ -0,0 +1,58 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Parser\Parser\Match; + +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\skipSpace; +use function Parsica\Parsica\string; + +final class MatchParser +{ + /** @var Parser */ + private static Parser $i; + + /** @return Parser */ + public static function get(): Parser + { + return self::$i ??= 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/MatchArm/MatchArmParser.php b/src/Parser/Parser/MatchArm/MatchArmParser.php new file mode 100644 index 00000000..6d5de4d1 --- /dev/null +++ b/src/Parser/Parser/MatchArm/MatchArmParser.php @@ -0,0 +1,61 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Parser\Parser\MatchArm; + +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; +use function Parsica\Parsica\either; +use function Parsica\Parsica\many; +use function Parsica\Parsica\skipSpace; +use function Parsica\Parsica\string; + +final class MatchArmParser +{ + /** @return Parser */ + public static function get(): Parser + { + return many( + self::getMatchArmParser() + )->map(fn ($matchArmNodes) => new MatchArmNodes(...$matchArmNodes ? $matchArmNodes : [])); + } + + private static function getMatchArmParser(): Parser + { + return collect( + either( + string('default')->voidLeft(null), + ExpressionsParser::get() + ), + skipSpace(), + string('->'), + skipSpace(), + ExpressionParser::get(), + skipSpace(), + )->map(fn ($collected) => new MatchArmNode($collected[0], $collected[4])); + } +} diff --git a/src/Parser/Parser/Module/ModuleParser.php b/src/Parser/Parser/Module/ModuleParser.php new file mode 100644 index 00000000..c9f3f5b0 --- /dev/null +++ b/src/Parser/Parser/Module/ModuleParser.php @@ -0,0 +1,76 @@ +. + */ + +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\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\Parser\UtilityParser; +use Parsica\Parsica\Parser; +use Parsica\Parsica\Stream; + +use function Parsica\Parsica\either; +use function Parsica\Parsica\many; + +final class ModuleParser +{ + /** @var Parser */ + private static Parser $i; + + public static function parseFromStream(Stream $stream): ModuleNode + { + return self::get()->thenEof()->try($stream)->output(); + } + + public static function parseFromString(string $string): ModuleNode + { + return self::get()->thenEof()->tryString($string)->output(); + } + + /** @return Parser */ + public static function get(): Parser + { + return self::$i ??= UtilityParser::skipSpaceAndComments()->then(many( + either( + ImportParser::get(), + 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/NullLiteral/NullLiteralParser.php b/src/Parser/Parser/NullLiteral/NullLiteralParser.php new file mode 100644 index 00000000..5c392197 --- /dev/null +++ b/src/Parser/Parser/NullLiteral/NullLiteralParser.php @@ -0,0 +1,40 @@ +. + */ + +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 +{ + /** @var Parser */ + private static Parser $i; + + /** @return Parser */ + public static function get(): Parser + { + return self::$i ??= string('null')->voidLeft(new NullLiteralNode()); + } +} diff --git a/src/Parser/Parser/NumberLiteral/NumberLiteralParser.php b/src/Parser/Parser/NumberLiteral/NumberLiteralParser.php new file mode 100644 index 00000000..80e1617a --- /dev/null +++ b/src/Parser/Parser/NumberLiteral/NumberLiteralParser.php @@ -0,0 +1,101 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Parser\Parser\NumberLiteral; + +use PackageFactory\ComponentEngine\Definition\NumberFormat; +use PackageFactory\ComponentEngine\Parser\Ast\NumberLiteralNode; +use Parsica\Parsica\Parser; + +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\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; + +final class NumberLiteralParser +{ + /** @var Parser */ + private static Parser $i; + + /** @return Parser */ + public static function get(): Parser + { + return self::$i ??= self::initialize(); + } + + /** @return Parser */ + private static function initialize(): Parser + { + $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( + $digits, + optional($fraction), + optional($exponent) + ), + append( + $fraction, + optional($exponent) + ) + )->label("float"); + } +} 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/PropertyDeclaration/PropertyDeclarationParser.php b/src/Parser/Parser/PropertyDeclaration/PropertyDeclarationParser.php new file mode 100644 index 00000000..8d5af3de --- /dev/null +++ b/src/Parser/Parser/PropertyDeclaration/PropertyDeclarationParser.php @@ -0,0 +1,52 @@ +. + */ + +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 +{ + /** @return Parser */ + public static function get(): 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 ?? [])); + } +} diff --git a/src/Parser/Parser/StringLiteral/StringLiteralParser.php b/src/Parser/Parser/StringLiteral/StringLiteralParser.php new file mode 100644 index 00000000..c272c91d --- /dev/null +++ b/src/Parser/Parser/StringLiteral/StringLiteralParser.php @@ -0,0 +1,40 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Parser\Parser\StringLiteral; + +use PackageFactory\ComponentEngine\Parser\Ast\StringLiteralNode; +use PackageFactory\ComponentEngine\Parser\Parser\UtilityParser; +use Parsica\Parsica\Parser; + +final class StringLiteralParser +{ + /** @var Parser */ + private static Parser $i; + + /** @return Parser */ + public static function get(): Parser + { + 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 new file mode 100644 index 00000000..626408d2 --- /dev/null +++ b/src/Parser/Parser/StructDeclaration/StructDeclarationParser.php @@ -0,0 +1,63 @@ +. + */ + +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 +{ + /** @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::$i ??= 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/Tag/TagParser.php b/src/Parser/Parser/Tag/TagParser.php new file mode 100644 index 00000000..f36b1535 --- /dev/null +++ b/src/Parser/Parser/Tag/TagParser.php @@ -0,0 +1,80 @@ +. + */ + +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 +{ + /** @var Parser */ + private static Parser $i; + + /** @return Parser */ + public static function get(): Parser + { + return self::$i ??= char('<')->sequence( + 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..34f55a68 --- /dev/null +++ b/src/Parser/Parser/TagContent/TagContentParser.php @@ -0,0 +1,57 @@ +. + */ + +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\many; +use function Parsica\Parsica\skipSpace; + +final class TagContentParser +{ + /** @return Parser */ + public static function get(): Parser + { + return skipSpace()->sequence( + many( + self::tagContent() + )->map(fn ($collected) => new TagContentNodes(...array_filter($collected ?? []))) + ); + } + + private static function tagContent(): Parser + { + return any( + TagParser::get(), + TextParser::get(), + char('{')->followedBy(ExpressionParser::get())->thenIgnore(char('}')), + )->map(fn ($item) => $item ? new TagContentNode($item) : null); + } +} diff --git a/src/Parser/Parser/TemplateLiteral/TemplateLiteralParser.php b/src/Parser/Parser/TemplateLiteral/TemplateLiteralParser.php new file mode 100644 index 00000000..077c97a4 --- /dev/null +++ b/src/Parser/Parser/TemplateLiteral/TemplateLiteralParser.php @@ -0,0 +1,65 @@ +. + */ + +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 +{ + /** @var Parser */ + private static Parser $i; + + /** @return Parser */ + public static function get(): Parser + { + return self::$i ??= char('`')->sequence( + zeroOrMore( + any( + self::stringLiteral(), + self::expression(), + )->map(fn ($item) => [$item]) + )->map(fn ($collected) => new TemplateLiteralNode(...$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/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/Text/TextParser.php b/src/Parser/Parser/Text/TextParser.php new file mode 100644 index 00000000..3b265fe9 --- /dev/null +++ b/src/Parser/Parser/Text/TextParser.php @@ -0,0 +1,58 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Parser\Parser\Text; + +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 +{ + /** @return Parser */ + public static function get(): Parser + { + return + collect( + takeWhile1( + // @todo handling of valid chars + fn ($char) => $char !== '<' && $char !== '{' + ), + lookAhead(anySingle()->append(anySingle())) + ) + ->map(function ($collected) { + [$text, $nextChars] = $collected; + if ($nextChars === '. + */ + +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 +{ + /** @return Parser */ + public static function get(): 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], + )); + } +} diff --git a/src/Parser/Parser/UnaryOperation/UnaryOperationParser.php b/src/Parser/Parser/UnaryOperation/UnaryOperationParser.php new file mode 100644 index 00000000..581fc9f4 --- /dev/null +++ b/src/Parser/Parser/UnaryOperation/UnaryOperationParser.php @@ -0,0 +1,49 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Parser\Parser\UnaryOperation; + +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; + +final class UnaryOperationParser +{ + /** @var Parser */ + private static Parser $i; + + /** @return Parser */ + public static function get(): Parser + { + return self::$i ??= self::unaryOperator()->bind(function (UnaryOperator $unaryOperator) { + return ExpressionParser::get($unaryOperator->toPrecedence())->map(fn ($expression) => new UnaryOperationNode($unaryOperator, $expression)); + }); + } + + private static function unaryOperator(): Parser + { + return char('!')->map(fn () => UnaryOperator::NOT); + } +} diff --git a/src/Parser/Parser/UtilityParser.php b/src/Parser/Parser/UtilityParser.php new file mode 100644 index 00000000..991ec529 --- /dev/null +++ b/src/Parser/Parser/UtilityParser.php @@ -0,0 +1,145 @@ +. + */ + +declare(strict_types=1); + +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; +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\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 +{ + public static function identifier(): Parser + { + return alphaChar()->append(zeroOrMore(alphaNumChar())); + } + + 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 + */ + 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()} + * + * 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); + }); + } + + /** + * 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 71c10271..0f7cd39e 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 static 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 07b98195..c2bb4912 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 static 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 . '"' ); }