Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 76 additions & 3 deletions src/TypeSystem/Resolver/Match/MatchTypeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
);
Expand All @@ -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<array{0:IdentifierNode, 1:IdentifierNode}>
*/
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(
Expand All @@ -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)
};
}
Expand Down
3 changes: 3 additions & 0 deletions src/TypeSystem/Type/EnumType/EnumStaticType.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ enumName: $enumDeclarationNode->enumName

public function is(TypeInterface $other): bool
{
if ($other instanceof EnumType) {
return $other->is($this);
}
return false;
}
}
20 changes: 16 additions & 4 deletions src/TypeSystem/Type/EnumType/EnumType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
}
}
153 changes: 147 additions & 6 deletions test/Unit/TypeSystem/Resolver/Match/MatchTypeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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())
],
];
Expand All @@ -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
Expand All @@ -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);
}
}