diff --git a/src/TypeSystem/Resolver/Match/MatchTypeResolver.php b/src/TypeSystem/Resolver/Match/MatchTypeResolver.php index ee24cd7c..3f04e13a 100644 --- a/src/TypeSystem/Resolver/Match/MatchTypeResolver.php +++ b/src/TypeSystem/Resolver/Match/MatchTypeResolver.php @@ -22,11 +22,18 @@ namespace PackageFactory\ComponentEngine\TypeSystem\Resolver\Match; +use PackageFactory\ComponentEngine\Definition\AccessType; +use PackageFactory\ComponentEngine\Parser\Ast\AccessNode; use PackageFactory\ComponentEngine\Parser\Ast\BooleanLiteralNode; +use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNode; +use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNodes; +use PackageFactory\ComponentEngine\Parser\Ast\IdentifierNode; use PackageFactory\ComponentEngine\Parser\Ast\MatchNode; use PackageFactory\ComponentEngine\TypeSystem\Resolver\Expression\ExpressionTypeResolver; +use PackageFactory\ComponentEngine\TypeSystem\Resolver\Identifier\IdentifierTypeResolver; use PackageFactory\ComponentEngine\TypeSystem\ScopeInterface; 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\UnionType\UnionType; use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; @@ -67,7 +74,14 @@ private function resolveTypeOfBooleanMatch(MatchNode $matchNode): TypeInterface } else { $types = []; + $defaultArmPresent = false; foreach ($matchNode->arms->items as $matchArmNode) { + if ($defaultArmPresent) { + throw new \Exception('@TODO: Multiple illegal default arms'); + } + if ($matchArmNode->left === null) { + $defaultArmPresent = true; + } $types[] = $expressionTypeResolver->resolveTypeOf( $matchArmNode->right ); @@ -79,24 +93,83 @@ private function resolveTypeOfBooleanMatch(MatchNode $matchNode): TypeInterface } } - private function resolveTypeOfEnumMatch(MatchNode $matchNode): TypeInterface + private function resolveTypeOfEnumMatch(MatchNode $matchNode, EnumType $subjectEnumType): TypeInterface { $expressionTypeResolver = new ExpressionTypeResolver( scope: $this->scope ); $types = []; + $defaultArmPresent = false; + $matchedEnumMembers = []; + foreach ($matchNode->arms->items as $matchArmNode) { + if ($defaultArmPresent) { + throw new \Exception('@TODO Error: Multiple illegal default arms'); + } + if ($matchArmNode->left === null) { + $defaultArmPresent = true; + } else { + foreach ($this->extractEnumTypeIdentifierAndEnumMemberIdentifier($matchArmNode->left) as [$enumIdentifier, $enumPath]) { + $enumType = (new IdentifierTypeResolver(scope: $this->scope))->resolveTypeOf($enumIdentifier); + + if (!$enumType instanceof EnumStaticType) { + throw new \Exception('@TODO Error: To be matched enum must be referenced static'); + } + + if (!$enumType->is($subjectEnumType)) { + throw new \Error('@TODO Error: incompatible enum match: got ' . $enumType->enumName . ' expected ' . $subjectEnumType->enumName); + } + + if (isset($matchedEnumMembers[$enumPath->value])) { + throw new \Error('@TODO Error: Enum path ' . $enumPath->value . ' was already defined once in this match and cannot be used twice'); + } + + $matchedEnumMembers[$enumPath->value] = true; + } + } + $types[] = $expressionTypeResolver->resolveTypeOf( $matchArmNode->right ); } - // @TODO: Ensure that match is complete + if (!$defaultArmPresent) { + foreach ($subjectEnumType->members as $member) { + if (!isset($matchedEnumMembers[$member])) { + throw new \Error('@TODO Error: member ' . $member . ' not checked'); + } + } + } return UnionType::of(...$types); } + /** + * @return \Iterator + */ + private function extractEnumTypeIdentifierAndEnumMemberIdentifier(ExpressionNodes $left) + { + foreach ($left->items as $expressionNode) { + $accessNode = $expressionNode->root; + if ( + !($accessNode instanceof AccessNode + && $accessNode->root instanceof ExpressionNode + && $accessNode->root->root instanceof IdentifierNode + && count($accessNode->chain->items) === 1 + && $accessNode->chain->items[0]->accessType === AccessType::MANDATORY + ) + ) { + throw new \Error('@TODO Error: To be matched enum value should be referenced like: `Enum.B`'); + } + + yield [ + $accessNode->root->root, + $accessNode->chain->items[0]->accessor + ]; + } + } + public function resolveTypeOf(MatchNode $matchNode): TypeInterface { $expressionTypeResolver = new ExpressionTypeResolver( @@ -108,7 +181,7 @@ public function resolveTypeOf(MatchNode $matchNode): TypeInterface return match (true) { BooleanType::get()->is($typeOfSubject) => $this->resolveTypeOfBooleanMatch($matchNode), - $typeOfSubject instanceof EnumType => $this->resolveTypeOfEnumMatch($matchNode), + $typeOfSubject instanceof EnumType => $this->resolveTypeOfEnumMatch($matchNode, $typeOfSubject), default => throw new \Exception('@TODO: Not handled ' . $typeOfSubject::class) }; } diff --git a/src/TypeSystem/Type/EnumType/EnumStaticType.php b/src/TypeSystem/Type/EnumType/EnumStaticType.php index 866e63a3..f2c13bd3 100644 --- a/src/TypeSystem/Type/EnumType/EnumStaticType.php +++ b/src/TypeSystem/Type/EnumType/EnumStaticType.php @@ -40,6 +40,9 @@ enumName: $enumDeclarationNode->enumName public function is(TypeInterface $other): bool { + if ($other instanceof EnumType) { + return $other->is($this); + } return false; } } diff --git a/src/TypeSystem/Type/EnumType/EnumType.php b/src/TypeSystem/Type/EnumType/EnumType.php index f83a28d6..b8293058 100644 --- a/src/TypeSystem/Type/EnumType/EnumType.php +++ b/src/TypeSystem/Type/EnumType/EnumType.php @@ -23,23 +23,35 @@ namespace PackageFactory\ComponentEngine\TypeSystem\Type\EnumType; use PackageFactory\ComponentEngine\Parser\Ast\EnumDeclarationNode; +use PackageFactory\ComponentEngine\Parser\Ast\EnumMemberDeclarationNode; use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; final class EnumType implements TypeInterface { - private function __construct(public readonly string $enumName) - { + private function __construct( + public readonly string $enumName, + public readonly array $members, + ) { } public static function fromEnumDeclarationNode(EnumDeclarationNode $enumDeclarationNode): self { return new self( - enumName: $enumDeclarationNode->enumName + enumName: $enumDeclarationNode->enumName, + members: array_map( + fn (EnumMemberDeclarationNode $memberDeclarationNode) => $memberDeclarationNode->name, + $enumDeclarationNode->memberDeclarations->items + ) ); } public function is(TypeInterface $other): bool { - return false; + // todo more satisfied check with namespace taken into account + return match ($other::class) { + EnumType::class => $this->enumName === $other->enumName, + EnumStaticType::class => $this->enumName === $other->enumName, + default => false + }; } } diff --git a/test/Unit/TypeSystem/Resolver/Match/MatchTypeResolverTest.php b/test/Unit/TypeSystem/Resolver/Match/MatchTypeResolverTest.php index ef872b07..d29b306f 100644 --- a/test/Unit/TypeSystem/Resolver/Match/MatchTypeResolverTest.php +++ b/test/Unit/TypeSystem/Resolver/Match/MatchTypeResolverTest.php @@ -28,6 +28,7 @@ use PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Scope\Fixtures\DummyScope; use PackageFactory\ComponentEngine\TypeSystem\Resolver\Match\MatchTypeResolver; 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\StringType\StringType; @@ -59,8 +60,24 @@ public function matchExamples(): array 'match (variableOfTypeBoolean) { true -> variableOfTypeNumber false -> variableOfTypeString }', UnionType::of(NumberType::get(), StringType::get()) ], - 'match (someEnumValue) { SomeEnum.A -> variableOfTypeNumber SomeEnum.B -> variableOfTypeString SomeEnum.C -> variableOfTypeBoolean }' => [ - 'match (someEnumValue) { SomeEnum.A -> variableOfTypeNumber SomeEnum.B -> variableOfTypeString SomeEnum.C -> variableOfTypeBoolean }', + 'match enum with all declared members' => [ + <<<'EOF' + match (someEnumValue) { + SomeEnum.A -> variableOfTypeNumber + SomeEnum.B -> variableOfTypeString + SomeEnum.C -> variableOfTypeBoolean + } + EOF, + UnionType::of(NumberType::get(), StringType::get(), BooleanType::get()) + ], + 'match enum with some declared members and default' => [ + <<<'EOF' + match (someEnumValue) { + SomeEnum.A -> variableOfTypeNumber + SomeEnum.B -> variableOfTypeString + default -> variableOfTypeBoolean + } + EOF, UnionType::of(NumberType::get(), StringType::get(), BooleanType::get()) ], ]; @@ -75,16 +92,21 @@ public function matchExamples(): array */ public function resolvesMatchToResultingType(string $matchAsString, TypeInterface $expectedType): void { + $someEnumDeclaration = EnumDeclarationNode::fromString( + 'enum SomeEnum { A B C }' + ); $someEnumType = EnumType::fromEnumDeclarationNode( - EnumDeclarationNode::fromString( - 'enum SomeEnum { A B C }' - ) + $someEnumDeclaration + ); + $someStaticEnumType = EnumStaticType::fromEnumDeclarationNode( + $someEnumDeclaration ); $scope = new DummyScope([ 'variableOfTypeBoolean' => BooleanType::get(), 'variableOfTypeString' => StringType::get(), 'variableOfTypeNumber' => NumberType::get(), - 'someEnumValue' => $someEnumType + 'someEnumValue' => $someEnumType, + 'SomeEnum' => $someStaticEnumType ]); $matchTypeResolver = new MatchTypeResolver( scope: $scope @@ -99,4 +121,123 @@ public function resolvesMatchToResultingType(string $matchAsString, TypeInterfac sprintf('Expected %s, got %s', $expectedType::class, $actualType::class) ); } + + + public function malformedEnumExamples(): iterable + { + yield "Multiple default keys" => [ + <<<'EOF' + match (someEnumValue) { + SomeEnum.A -> "a" + default -> "b" + default -> "c" + } + EOF, + "@TODO Error: Multiple illegal default arms" + ]; + + yield "Missing match" => [ + <<<'EOF' + match (someEnumValue) { + SomeEnum.A -> "a" + SomeEnum.B -> "a" + } + EOF, + "@TODO Error: member C not checked" + ]; + + // @todo fails as not implemented (could be done hackish wrong + // - but this should be validated at all places where enums are used) + yield "Non existent enum member access" => [ + <<<'EOF' + match (someEnumValue) { + SomeEnum.A -> "a" + SomeEnum.B -> "a" + SomeEnum.C -> "a" + SomeEnum.NonExistent -> "a" + } + EOF, + "@TODO Error: `SomeEnum.NonExistent` is not a valid member." + ]; + + yield "Duplicate match 1" => [ + <<<'EOF' + match (someEnumValue) { + SomeEnum.A -> "a" + SomeEnum.A -> "a" + } + EOF, + "@TODO Error: Enum path A was already defined once in this match and cannot be used twice" + ]; + + yield "Duplicate match 2" => [ + <<<'EOF' + match (someEnumValue) { + SomeEnum.A, SomeEnum.A -> "a" + } + EOF, + "@TODO Error: Enum path A was already defined once in this match and cannot be used twice" + ]; + + yield "Incompatible enum types" => [ + <<<'EOF' + match (someEnumValue) { + OtherEnum.A -> "a" + } + EOF, + "@TODO Error: incompatible enum match: got OtherEnum expected SomeEnum" + ]; + + yield "Cant match enum and string" => [ + <<<'EOF' + match (someEnumValue) { + "foo" -> "a" + } + EOF, + "@TODO Error: To be matched enum value should be referenced like: `Enum.B`" + ]; + + yield "Cant match enum and non static enum" => [ + <<<'EOF' + match (someEnumValue) { + someEnumValue.a -> "a" + } + EOF, + "@TODO Error: To be matched enum must be referenced static" + ]; + } + + /** + * @dataProvider malformedEnumExamples + * @test + */ + public function malformedMatchCannotBeResolved(string $matchAsString, string $expectedErrorMessage) + { + $this->expectExceptionMessage($expectedErrorMessage); + $someEnumDeclaration = EnumDeclarationNode::fromString( + 'enum SomeEnum { A B C }' + ); + $someEnumType = EnumType::fromEnumDeclarationNode( + $someEnumDeclaration + ); + $someStaticEnumType = EnumStaticType::fromEnumDeclarationNode( + $someEnumDeclaration + ); + $scope = new DummyScope([ + 'someEnumValue' => $someEnumType, + 'SomeEnum' => $someStaticEnumType, + 'OtherEnum' => EnumStaticType::fromEnumDeclarationNode( + EnumDeclarationNode::fromString('enum OtherEnum { A }') + ) + + ]); + + $matchTypeResolver = new MatchTypeResolver( + scope: $scope + ); + $matchNode = ExpressionNode::fromString($matchAsString)->root; + assert($matchNode instanceof MatchNode); + + $matchTypeResolver->resolveTypeOf($matchNode); + } }