From 7efcfd3cc179443912b5e06d1b913efde3cb5739 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 9 Apr 2023 15:45:27 +0200 Subject: [PATCH 01/28] TASK: Put domain logic inside constructor of UnionType --- src/TypeSystem/Type/UnionType/UnionType.php | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/TypeSystem/Type/UnionType/UnionType.php b/src/TypeSystem/Type/UnionType/UnionType.php index d104c79e..64e1f0cb 100644 --- a/src/TypeSystem/Type/UnionType/UnionType.php +++ b/src/TypeSystem/Type/UnionType/UnionType.php @@ -32,11 +32,6 @@ final class UnionType implements TypeInterface private array $members; private function __construct(TypeInterface ...$members) - { - $this->members = $members; - } - - public static function of(TypeInterface ...$members): TypeInterface { $uniqueMembers = []; foreach ($members as $member) { @@ -48,9 +43,15 @@ public static function of(TypeInterface ...$members): TypeInterface $uniqueMembers[] = $member; } + $this->members = $uniqueMembers; + } + + public static function of(TypeInterface ...$members): TypeInterface + { + $union = new self(...$members); - if (count($uniqueMembers) === 1) { - return $uniqueMembers[0]; + if (count($union->members) === 1) { + return $union->members[0]; } return new self(...$members); From 2ffe099a2eb33ae4c59ab2ab507aa65d2c3444f2 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 9 Apr 2023 16:40:36 +0200 Subject: [PATCH 02/28] TASK: Simple Nullable Handling - Scopes will check if $typeReferenceNode->isOptional and if so, wrap the type in a union with null - The TypeReferenceTranspiler can now transpile those simple unions, by looking into its members --- .../TypeReference/TypeReferenceTranspiler.php | 42 +++++++++++++++---- .../Scope/GlobalScope/GlobalScope.php | 9 +++- .../Scope/ModuleScope/ModuleScope.php | 8 +++- src/TypeSystem/Type/UnionType/UnionType.php | 2 +- .../TypeSystem/Scope/Fixtures/DummyScope.php | 5 +++ 5 files changed, 55 insertions(+), 11 deletions(-) diff --git a/src/Target/Php/Transpiler/TypeReference/TypeReferenceTranspiler.php b/src/Target/Php/Transpiler/TypeReference/TypeReferenceTranspiler.php index af5a7326..046f25f3 100644 --- a/src/Target/Php/Transpiler/TypeReference/TypeReferenceTranspiler.php +++ b/src/Target/Php/Transpiler/TypeReference/TypeReferenceTranspiler.php @@ -27,10 +27,13 @@ use PackageFactory\ComponentEngine\TypeSystem\Type\BooleanType\BooleanType; use PackageFactory\ComponentEngine\TypeSystem\Type\ComponentType\ComponentType; use PackageFactory\ComponentEngine\TypeSystem\Type\EnumType\EnumType; +use PackageFactory\ComponentEngine\TypeSystem\Type\NullType\NullType; use PackageFactory\ComponentEngine\TypeSystem\Type\NumberType\NumberType; use PackageFactory\ComponentEngine\TypeSystem\Type\SlotType\SlotType; use PackageFactory\ComponentEngine\TypeSystem\Type\StringType\StringType; use PackageFactory\ComponentEngine\TypeSystem\Type\StructType\StructType; +use PackageFactory\ComponentEngine\TypeSystem\Type\UnionType\UnionType; +use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; final class TypeReferenceTranspiler { @@ -43,7 +46,30 @@ public function __construct( public function transpile(TypeReferenceNode $typeReferenceNode): string { $type = $this->scope->resolveTypeReference($typeReferenceNode); - $phpTypeReference = match ($type::class) { + + return match ($type::class) { + UnionType::class => $this->transpileUnionType($type, $typeReferenceNode), + default => $this->transpileNonUnionType($type, $typeReferenceNode) + }; + } + + private function transpileUnionType(UnionType $unionType, TypeReferenceNode $typeReferenceNode): string + { + if (count($unionType->members) === 2 && $otherMemberTypeIfOneMemberIsNullType = match (NullType::class) { + $unionType->members[0]::class => $unionType->members[1], + $unionType->members[1]::class => $unionType->members[0], + default => null + }) { + return $this->transpileNullableType($otherMemberTypeIfOneMemberIsNullType, $typeReferenceNode); + } + + throw new \Exception('@TODO Transpilation of complex union types is not implemented'); + + } + + private function transpileNonUnionType(TypeInterface $type, TypeReferenceNode $typeReferenceNode): string + { + return match ($type::class) { NumberType::class => 'int|float', StringType::class => 'string', BooleanType::class => 'bool', @@ -51,14 +77,16 @@ public function transpile(TypeReferenceNode $typeReferenceNode): string ComponentType::class => $this->strategy->getPhpTypeReferenceForComponentType($type, $typeReferenceNode), EnumType::class => $this->strategy->getPhpTypeReferenceForEnumType($type, $typeReferenceNode), StructType::class => $this->strategy->getPhpTypeReferenceForStructType($type, $typeReferenceNode), + UnionType::class => throw new \Exception("There is no such thing as nested unions, think again."), default => $this->strategy->getPhpTypeReferenceForCustomType($type, $typeReferenceNode) }; + } - return $typeReferenceNode->isOptional - ? match ($phpTypeReference) { - 'int|float' => 'null|int|float', - default => '?' . $phpTypeReference - } - : $phpTypeReference; + private function transpileNullableType(TypeInterface $type, TypeReferenceNode $typeReferenceNode): string + { + if ($type->is(NumberType::get())) { + return 'null|int|float'; + } + return '?' . $this->transpileNonUnionType($type, $typeReferenceNode); } } diff --git a/src/TypeSystem/Scope/GlobalScope/GlobalScope.php b/src/TypeSystem/Scope/GlobalScope/GlobalScope.php index a6ab1b87..ada0f5cb 100644 --- a/src/TypeSystem/Scope/GlobalScope/GlobalScope.php +++ b/src/TypeSystem/Scope/GlobalScope/GlobalScope.php @@ -22,13 +22,14 @@ namespace PackageFactory\ComponentEngine\TypeSystem\Scope\GlobalScope; -use PackageFactory\ComponentEngine\Parser\Ast\ComponentDeclarationNode; use PackageFactory\ComponentEngine\Parser\Ast\TypeReferenceNode; use PackageFactory\ComponentEngine\TypeSystem\ScopeInterface; use PackageFactory\ComponentEngine\TypeSystem\Type\BooleanType\BooleanType; +use PackageFactory\ComponentEngine\TypeSystem\Type\NullType\NullType; use PackageFactory\ComponentEngine\TypeSystem\Type\NumberType\NumberType; use PackageFactory\ComponentEngine\TypeSystem\Type\SlotType\SlotType; use PackageFactory\ComponentEngine\TypeSystem\Type\StringType\StringType; +use PackageFactory\ComponentEngine\TypeSystem\Type\UnionType\UnionType; use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; final class GlobalScope implements ScopeInterface @@ -51,12 +52,16 @@ public function lookupTypeFor(string $name): ?TypeInterface public function resolveTypeReference(TypeReferenceNode $typeReferenceNode): TypeInterface { - return match ($typeReferenceNode->name) { + $type = match ($typeReferenceNode->name) { 'string' => StringType::get(), 'number' => NumberType::get(), 'boolean' => BooleanType::get(), 'slot' => SlotType::get(), default => throw new \Exception('@TODO: Unknown Type ' . $typeReferenceNode->name) }; + if ($typeReferenceNode->isOptional) { + $type = UnionType::of($type, NullType::get()); + } + return $type; } } diff --git a/src/TypeSystem/Scope/ModuleScope/ModuleScope.php b/src/TypeSystem/Scope/ModuleScope/ModuleScope.php index 034da99e..80748ea2 100644 --- a/src/TypeSystem/Scope/ModuleScope/ModuleScope.php +++ b/src/TypeSystem/Scope/ModuleScope/ModuleScope.php @@ -26,6 +26,8 @@ use PackageFactory\ComponentEngine\Parser\Ast\ModuleNode; use PackageFactory\ComponentEngine\Parser\Ast\TypeReferenceNode; use PackageFactory\ComponentEngine\TypeSystem\ScopeInterface; +use PackageFactory\ComponentEngine\TypeSystem\Type\NullType\NullType; +use PackageFactory\ComponentEngine\TypeSystem\Type\UnionType\UnionType; use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; final class ModuleScope implements ScopeInterface @@ -45,7 +47,11 @@ public function lookupTypeFor(string $name): ?TypeInterface public function resolveTypeReference(TypeReferenceNode $typeReferenceNode): TypeInterface { if ($importNode = $this->moduleNode->imports->get($typeReferenceNode->name)) { - return $this->loader->resolveTypeOfImport($importNode); + $type = $this->loader->resolveTypeOfImport($importNode); + if ($typeReferenceNode->isOptional) { + $type = UnionType::of($type, NullType::get()); + } + return $type; } if ($this->parentScope) { diff --git a/src/TypeSystem/Type/UnionType/UnionType.php b/src/TypeSystem/Type/UnionType/UnionType.php index 64e1f0cb..fe6b1c7f 100644 --- a/src/TypeSystem/Type/UnionType/UnionType.php +++ b/src/TypeSystem/Type/UnionType/UnionType.php @@ -29,7 +29,7 @@ final class UnionType implements TypeInterface /** * @var TypeInterface[] */ - private array $members; + public array $members; private function __construct(TypeInterface ...$members) { diff --git a/test/Unit/TypeSystem/Scope/Fixtures/DummyScope.php b/test/Unit/TypeSystem/Scope/Fixtures/DummyScope.php index 954a1f3c..cb3440b9 100644 --- a/test/Unit/TypeSystem/Scope/Fixtures/DummyScope.php +++ b/test/Unit/TypeSystem/Scope/Fixtures/DummyScope.php @@ -24,6 +24,8 @@ use PackageFactory\ComponentEngine\Parser\Ast\TypeReferenceNode; use PackageFactory\ComponentEngine\TypeSystem\ScopeInterface; +use PackageFactory\ComponentEngine\TypeSystem\Type\NullType\NullType; +use PackageFactory\ComponentEngine\TypeSystem\Type\UnionType\UnionType; use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; final class DummyScope implements ScopeInterface @@ -46,6 +48,9 @@ public function lookupTypeFor(string $name): ?TypeInterface public function resolveTypeReference(TypeReferenceNode $typeReferenceNode): TypeInterface { if ($type = $this->typeNameToTypeMap[$typeReferenceNode->name] ?? null) { + if ($typeReferenceNode->isOptional) { + $type = UnionType::of($type, NullType::get()); + } return $type; } From 6293145f6ccd137c10a700d9c83bc9ed05cb0e92 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 9 Apr 2023 17:28:26 +0200 Subject: [PATCH 03/28] TASK: Infer types in arms of ternary `nullableString ? nullableString : "fallback"` will be a string --- .../TypeReference/TypeReferenceTranspiler.php | 14 +++-- .../TernaryOperationTypeResolver.php | 34 +++++++++++-- .../Scope/ShallowScope/ShallowScope.php | 51 +++++++++++++++++++ src/TypeSystem/Type/UnionType/UnionType.php | 28 +++++++++- .../TernaryOperationTypeResolverTest.php | 10 +++- 5 files changed, 124 insertions(+), 13 deletions(-) create mode 100644 src/TypeSystem/Scope/ShallowScope/ShallowScope.php diff --git a/src/Target/Php/Transpiler/TypeReference/TypeReferenceTranspiler.php b/src/Target/Php/Transpiler/TypeReference/TypeReferenceTranspiler.php index 046f25f3..30cbabbe 100644 --- a/src/Target/Php/Transpiler/TypeReference/TypeReferenceTranspiler.php +++ b/src/Target/Php/Transpiler/TypeReference/TypeReferenceTranspiler.php @@ -27,7 +27,6 @@ use PackageFactory\ComponentEngine\TypeSystem\Type\BooleanType\BooleanType; use PackageFactory\ComponentEngine\TypeSystem\Type\ComponentType\ComponentType; use PackageFactory\ComponentEngine\TypeSystem\Type\EnumType\EnumType; -use PackageFactory\ComponentEngine\TypeSystem\Type\NullType\NullType; use PackageFactory\ComponentEngine\TypeSystem\Type\NumberType\NumberType; use PackageFactory\ComponentEngine\TypeSystem\Type\SlotType\SlotType; use PackageFactory\ComponentEngine\TypeSystem\Type\StringType\StringType; @@ -55,16 +54,15 @@ public function transpile(TypeReferenceNode $typeReferenceNode): string private function transpileUnionType(UnionType $unionType, TypeReferenceNode $typeReferenceNode): string { - if (count($unionType->members) === 2 && $otherMemberTypeIfOneMemberIsNullType = match (NullType::class) { - $unionType->members[0]::class => $unionType->members[1], - $unionType->members[1]::class => $unionType->members[0], - default => null - }) { - return $this->transpileNullableType($otherMemberTypeIfOneMemberIsNullType, $typeReferenceNode); + if ($unionType->isNullable()) { + $nonNullable = $unionType->withoutNullable(); + if ($nonNullable instanceof UnionType) { + throw new \Exception('@TODO Transpilation of nullable union types with more non null members is not implemented'); + } + return $this->transpileNullableType($nonNullable, $typeReferenceNode); } throw new \Exception('@TODO Transpilation of complex union types is not implemented'); - } private function transpileNonUnionType(TypeInterface $type, TypeReferenceNode $typeReferenceNode): string diff --git a/src/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolver.php b/src/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolver.php index c3e6d8ff..f8dae1e2 100644 --- a/src/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolver.php +++ b/src/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolver.php @@ -23,9 +23,12 @@ namespace PackageFactory\ComponentEngine\TypeSystem\Resolver\TernaryOperation; use PackageFactory\ComponentEngine\Parser\Ast\BooleanLiteralNode; +use PackageFactory\ComponentEngine\Parser\Ast\IdentifierNode; use PackageFactory\ComponentEngine\Parser\Ast\TernaryOperationNode; use PackageFactory\ComponentEngine\TypeSystem\Resolver\Expression\ExpressionTypeResolver; +use PackageFactory\ComponentEngine\TypeSystem\Scope\ShallowScope\ShallowScope; use PackageFactory\ComponentEngine\TypeSystem\ScopeInterface; +use PackageFactory\ComponentEngine\TypeSystem\Type\NullType\NullType; use PackageFactory\ComponentEngine\TypeSystem\Type\UnionType\UnionType; use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; @@ -41,14 +44,39 @@ public function resolveTypeOf(TernaryOperationNode $ternaryOperationNode): TypeI $expressionTypeResolver = new ExpressionTypeResolver( scope: $this->scope ); - $conditionNode = $ternaryOperationNode->condition->root; + $conditionNode = $ternaryOperationNode->condition; - if ($conditionNode instanceof BooleanLiteralNode) { - return $conditionNode->value + $rootType = $expressionTypeResolver->resolveTypeOf($conditionNode); + + if ($conditionNode->root instanceof BooleanLiteralNode) { + return $conditionNode->root->value ? $expressionTypeResolver->resolveTypeOf($ternaryOperationNode->true) : $expressionTypeResolver->resolveTypeOf($ternaryOperationNode->false); } + if ($conditionNode->root instanceof IdentifierNode && $rootType instanceof UnionType && $rootType->isNullable()) { + $trueExpressionTypeResolver = new ExpressionTypeResolver( + scope: new ShallowScope( + $conditionNode->root->value, + $rootType->withoutNullable(), + $this->scope + ) + ); + + $falseExpressionTypeResolver = new ExpressionTypeResolver( + scope: new ShallowScope( + $conditionNode->root->value, + NullType::get(), + $this->scope + ) + ); + + return UnionType::of( + $trueExpressionTypeResolver->resolveTypeOf($ternaryOperationNode->true), + $falseExpressionTypeResolver->resolveTypeOf($ternaryOperationNode->false) + ); + } + return UnionType::of( $expressionTypeResolver->resolveTypeOf($ternaryOperationNode->true), $expressionTypeResolver->resolveTypeOf($ternaryOperationNode->false) diff --git a/src/TypeSystem/Scope/ShallowScope/ShallowScope.php b/src/TypeSystem/Scope/ShallowScope/ShallowScope.php new file mode 100644 index 00000000..0c625d98 --- /dev/null +++ b/src/TypeSystem/Scope/ShallowScope/ShallowScope.php @@ -0,0 +1,51 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\TypeSystem\Scope\ShallowScope; + +use PackageFactory\ComponentEngine\Parser\Ast\TypeReferenceNode; +use PackageFactory\ComponentEngine\TypeSystem\ScopeInterface; +use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; + +final class ShallowScope implements ScopeInterface +{ + public function __construct( + private readonly string $overriddenName, + private readonly TypeInterface $overriddenType, + private readonly ScopeInterface $parentScope + ) { + } + + public function lookupTypeFor(string $name): ?TypeInterface + { + if ($this->overriddenName === $name) { + return $this->overriddenType; + } + + return $this->parentScope->lookupTypeFor($name); + } + + public function resolveTypeReference(TypeReferenceNode $typeReferenceNode): TypeInterface + { + return $this->parentScope->resolveTypeReference($typeReferenceNode); + } +} diff --git a/src/TypeSystem/Type/UnionType/UnionType.php b/src/TypeSystem/Type/UnionType/UnionType.php index fe6b1c7f..2fad9c6b 100644 --- a/src/TypeSystem/Type/UnionType/UnionType.php +++ b/src/TypeSystem/Type/UnionType/UnionType.php @@ -22,6 +22,7 @@ namespace PackageFactory\ComponentEngine\TypeSystem\Type\UnionType; +use PackageFactory\ComponentEngine\TypeSystem\Type\NullType\NullType; use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; final class UnionType implements TypeInterface @@ -29,7 +30,7 @@ final class UnionType implements TypeInterface /** * @var TypeInterface[] */ - public array $members; + private array $members; private function __construct(TypeInterface ...$members) { @@ -46,6 +47,31 @@ private function __construct(TypeInterface ...$members) $this->members = $uniqueMembers; } + public function isNullable(): bool + { + foreach ($this->members as $member) { + if ($member->is(NullType::get())) { + return true; + } + } + return false; + } + + public function withoutNullable(): TypeInterface + { + $nonNullMembers = []; + foreach ($this->members as $member) { + if ($member->is(NullType::get())) { + continue; + } + $nonNullMembers[] = $member; + } + if (count($nonNullMembers) === 1) { + return $nonNullMembers[0]; + } + return self::of(...$nonNullMembers); + } + public static function of(TypeInterface ...$members): TypeInterface { $union = new self(...$members); diff --git a/test/Unit/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolverTest.php b/test/Unit/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolverTest.php index 60845e64..14aab685 100644 --- a/test/Unit/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolverTest.php +++ b/test/Unit/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolverTest.php @@ -26,6 +26,7 @@ use PackageFactory\ComponentEngine\Parser\Ast\TernaryOperationNode; use PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Scope\Fixtures\DummyScope; use PackageFactory\ComponentEngine\TypeSystem\Resolver\TernaryOperation\TernaryOperationTypeResolver; +use PackageFactory\ComponentEngine\TypeSystem\Type\NullType\NullType; use PackageFactory\ComponentEngine\TypeSystem\Type\NumberType\NumberType; use PackageFactory\ComponentEngine\TypeSystem\Type\StringType\StringType; use PackageFactory\ComponentEngine\TypeSystem\Type\UnionType\UnionType; @@ -46,7 +47,13 @@ public function ternaryOperationExamples(): array '1 < 2 ? variableOfTypeString : variableOfTypeNumber' => [ '1 < 2 ? variableOfTypeString : variableOfTypeNumber', UnionType::of(NumberType::get(), StringType::get()) - ] + ], + 'nullableString ? nullableString : "fallback"' => [ + 'nullableString ? nullableString : "fallback"', StringType::get() + ], + 'nullableString ? null : nullableString' => [ + 'nullableString ? null : nullableString', NullType::get() + ], ]; } @@ -62,6 +69,7 @@ public function resolvesTernaryOperationToResultingType(string $ternaryExpressio $scope = new DummyScope([ 'variableOfTypeString' => StringType::get(), 'variableOfTypeNumber' => NumberType::get(), + 'nullableString' => UnionType::of(StringType::get(), NullType::get()) ]); $ternaryOperationTypeResolver = new TernaryOperationTypeResolver( scope: $scope From 2a08474ceade4a1f6f0cffb1673c1b349a227d57 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 9 Apr 2023 20:31:17 +0200 Subject: [PATCH 04/28] TASK: Revert 7efcfd3cc179443912b5e06d1b913efde3cb5739 --- src/TypeSystem/Type/UnionType/UnionType.php | 30 +++++++++++---------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/TypeSystem/Type/UnionType/UnionType.php b/src/TypeSystem/Type/UnionType/UnionType.php index 2fad9c6b..e91fa6d9 100644 --- a/src/TypeSystem/Type/UnionType/UnionType.php +++ b/src/TypeSystem/Type/UnionType/UnionType.php @@ -33,6 +33,14 @@ final class UnionType implements TypeInterface private array $members; private function __construct(TypeInterface ...$members) + { + if (count($members) < 1) { + throw new \Exception('UnionType can only hold more than one different members'); + } + $this->members = $members; + } + + public static function of(TypeInterface ...$members): TypeInterface { $uniqueMembers = []; foreach ($members as $member) { @@ -44,7 +52,12 @@ private function __construct(TypeInterface ...$members) $uniqueMembers[] = $member; } - $this->members = $uniqueMembers; + + if (count($uniqueMembers) === 1) { + return $uniqueMembers[0]; + } + + return new self(...$members); } public function isNullable(): bool @@ -72,17 +85,6 @@ public function withoutNullable(): TypeInterface return self::of(...$nonNullMembers); } - public static function of(TypeInterface ...$members): TypeInterface - { - $union = new self(...$members); - - if (count($union->members) === 1) { - return $union->members[0]; - } - - return new self(...$members); - } - public function is(TypeInterface $other): bool { if ($other instanceof UnionType) { @@ -94,12 +96,12 @@ public function is(TypeInterface $other): bool break; } } - + if (!$match) { return false; } } - + return true; } else { return false; From 5e4d6ce282743ad4f8c8586ef18f6992a1d319da Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 9 Apr 2023 21:01:33 +0200 Subject: [PATCH 05/28] TASK: Union test that all members are deduplicated --- src/TypeSystem/Type/UnionType/UnionType.php | 20 +++++++++++++--- .../Type/UnionType/UnionTypeTest.php | 23 ++++++++++++++++++- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/src/TypeSystem/Type/UnionType/UnionType.php b/src/TypeSystem/Type/UnionType/UnionType.php index e91fa6d9..8d0ebd39 100644 --- a/src/TypeSystem/Type/UnionType/UnionType.php +++ b/src/TypeSystem/Type/UnionType/UnionType.php @@ -25,12 +25,15 @@ use PackageFactory\ComponentEngine\TypeSystem\Type\NullType\NullType; use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; -final class UnionType implements TypeInterface +/** + * @implements \IteratorAggregate + */ +final class UnionType implements TypeInterface, \IteratorAggregate, \Countable { /** * @var TypeInterface[] */ - private array $members; + private readonly array $members; private function __construct(TypeInterface ...$members) { @@ -57,7 +60,7 @@ public static function of(TypeInterface ...$members): TypeInterface return $uniqueMembers[0]; } - return new self(...$members); + return new self(...$uniqueMembers); } public function isNullable(): bool @@ -107,4 +110,15 @@ public function is(TypeInterface $other): bool return false; } } + + /** @return \ArrayIterator */ + public function getIterator(): \ArrayIterator + { + return new \ArrayIterator($this->members); + } + + public function count(): int + { + return count($this->members); + } } diff --git a/test/Unit/TypeSystem/Type/UnionType/UnionTypeTest.php b/test/Unit/TypeSystem/Type/UnionType/UnionTypeTest.php index 105edf6a..7ec6eb0b 100644 --- a/test/Unit/TypeSystem/Type/UnionType/UnionTypeTest.php +++ b/test/Unit/TypeSystem/Type/UnionType/UnionTypeTest.php @@ -87,6 +87,27 @@ public function isReturnsTrueIfGivenTypeIsCongruentUnionTypeWithRedundantMembers $this->assertTrue($unionType->is($otherUnionType)); } + /** + * @test + */ + public function unionOnlyHoldsDeduplicatedMembers(): void + { + $unionType = UnionType::of(NumberType::get(), StringType::get()); + $otherUnionType = UnionType::of(NumberType::get(), StringType::get(), NumberType::get(), StringType::get()); + + $this->assertTrue($unionType->is($otherUnionType)); + + $this->assertInstanceOf(UnionType::class, $unionType); + $this->assertInstanceOf(UnionType::class, $otherUnionType); + + $this->assertCount(count($unionType), $otherUnionType); + + $this->assertEqualsCanonicalizing( + $unionType->getIterator()->getArrayCopy(), + $otherUnionType->getIterator()->getArrayCopy() + ); + } + /** * @test */ @@ -97,4 +118,4 @@ public function isReturnsFalseIfGivenTypeIsNotCongruent(): void $this->assertFalse($unionType->is(NumberType::get())); $this->assertFalse($unionType->is(StringType::get())); } -} \ No newline at end of file +} From 5c08a4e19a18eb6302e14d5515f072bd667c8720 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 9 Apr 2023 21:06:34 +0200 Subject: [PATCH 06/28] TASK: Union test isNullable and withoutNullable --- .../Type/UnionType/UnionTypeTest.php | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/test/Unit/TypeSystem/Type/UnionType/UnionTypeTest.php b/test/Unit/TypeSystem/Type/UnionType/UnionTypeTest.php index 7ec6eb0b..78afde25 100644 --- a/test/Unit/TypeSystem/Type/UnionType/UnionTypeTest.php +++ b/test/Unit/TypeSystem/Type/UnionType/UnionTypeTest.php @@ -22,6 +22,7 @@ namespace PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Type\UnionType; +use PackageFactory\ComponentEngine\TypeSystem\Type\NullType\NullType; use PackageFactory\ComponentEngine\TypeSystem\Type\NumberType\NumberType; use PackageFactory\ComponentEngine\TypeSystem\Type\StringType\StringType; use PackageFactory\ComponentEngine\TypeSystem\Type\UnionType\UnionType; @@ -118,4 +119,52 @@ public function isReturnsFalseIfGivenTypeIsNotCongruent(): void $this->assertFalse($unionType->is(NumberType::get())); $this->assertFalse($unionType->is(StringType::get())); } + + /** + * @test + */ + public function isNullableOnNullableString(): void + { + $unionType = UnionType::of(StringType::get(), NullType::get()); + + $this->assertInstanceOf(UnionType::class, $unionType); + + $this->assertTrue($unionType->isNullable()); + + $nonNullables = $unionType->withoutNullable(); + + $this->assertTrue($nonNullables->is(StringType::get())); + } + + /** + * @test + */ + public function isNullableWithMultipleItems(): void + { + $unionType = UnionType::of(StringType::get(), NumberType::get(), NullType::get()); + + $this->assertInstanceOf(UnionType::class, $unionType); + + $this->assertTrue($unionType->isNullable()); + + $nonNullables = $unionType->withoutNullable(); + + $this->assertTrue($nonNullables->is(UnionType::of(StringType::get(), NumberType::get()))); + } + + /** + * @test + */ + public function isNullableOnNonNullableUnion(): void + { + $unionType = UnionType::of(StringType::get(), NumberType::get()); + + $this->assertInstanceOf(UnionType::class, $unionType); + + $this->assertFalse($unionType->isNullable()); + + $nonNullables = $unionType->withoutNullable(); + + $this->assertTrue($nonNullables->is($unionType)); + } } From 8f0ba11082d9b774f929510997d0faa59629f7ca Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Fri, 21 Apr 2023 20:31:22 +0200 Subject: [PATCH 07/28] TASK: UnionType rename to containsNull, withoutNull --- .../TypeReference/TypeReferenceTranspiler.php | 8 ++++---- .../TernaryOperationTypeResolver.php | 6 +++--- src/TypeSystem/Type/UnionType/UnionType.php | 4 ++-- .../Unit/TypeSystem/Type/UnionType/UnionTypeTest.php | 12 ++++++------ 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Target/Php/Transpiler/TypeReference/TypeReferenceTranspiler.php b/src/Target/Php/Transpiler/TypeReference/TypeReferenceTranspiler.php index 30cbabbe..f205106a 100644 --- a/src/Target/Php/Transpiler/TypeReference/TypeReferenceTranspiler.php +++ b/src/Target/Php/Transpiler/TypeReference/TypeReferenceTranspiler.php @@ -51,11 +51,11 @@ public function transpile(TypeReferenceNode $typeReferenceNode): string default => $this->transpileNonUnionType($type, $typeReferenceNode) }; } - + private function transpileUnionType(UnionType $unionType, TypeReferenceNode $typeReferenceNode): string { - if ($unionType->isNullable()) { - $nonNullable = $unionType->withoutNullable(); + if ($unionType->containsNull()) { + $nonNullable = $unionType->withoutNull(); if ($nonNullable instanceof UnionType) { throw new \Exception('@TODO Transpilation of nullable union types with more non null members is not implemented'); } @@ -64,7 +64,7 @@ private function transpileUnionType(UnionType $unionType, TypeReferenceNode $typ throw new \Exception('@TODO Transpilation of complex union types is not implemented'); } - + private function transpileNonUnionType(TypeInterface $type, TypeReferenceNode $typeReferenceNode): string { return match ($type::class) { diff --git a/src/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolver.php b/src/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolver.php index f8dae1e2..f2b4242b 100644 --- a/src/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolver.php +++ b/src/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolver.php @@ -46,7 +46,7 @@ public function resolveTypeOf(TernaryOperationNode $ternaryOperationNode): TypeI ); $conditionNode = $ternaryOperationNode->condition; - $rootType = $expressionTypeResolver->resolveTypeOf($conditionNode); + $conditionType = $expressionTypeResolver->resolveTypeOf($conditionNode); if ($conditionNode->root instanceof BooleanLiteralNode) { return $conditionNode->root->value @@ -54,11 +54,11 @@ public function resolveTypeOf(TernaryOperationNode $ternaryOperationNode): TypeI : $expressionTypeResolver->resolveTypeOf($ternaryOperationNode->false); } - if ($conditionNode->root instanceof IdentifierNode && $rootType instanceof UnionType && $rootType->isNullable()) { + if ($conditionNode->root instanceof IdentifierNode && $conditionType instanceof UnionType && $conditionType->containsNull()) { $trueExpressionTypeResolver = new ExpressionTypeResolver( scope: new ShallowScope( $conditionNode->root->value, - $rootType->withoutNullable(), + $conditionType->withoutNull(), $this->scope ) ); diff --git a/src/TypeSystem/Type/UnionType/UnionType.php b/src/TypeSystem/Type/UnionType/UnionType.php index 8d0ebd39..8dd48cd7 100644 --- a/src/TypeSystem/Type/UnionType/UnionType.php +++ b/src/TypeSystem/Type/UnionType/UnionType.php @@ -63,7 +63,7 @@ public static function of(TypeInterface ...$members): TypeInterface return new self(...$uniqueMembers); } - public function isNullable(): bool + public function containsNull(): bool { foreach ($this->members as $member) { if ($member->is(NullType::get())) { @@ -73,7 +73,7 @@ public function isNullable(): bool return false; } - public function withoutNullable(): TypeInterface + public function withoutNull(): TypeInterface { $nonNullMembers = []; foreach ($this->members as $member) { diff --git a/test/Unit/TypeSystem/Type/UnionType/UnionTypeTest.php b/test/Unit/TypeSystem/Type/UnionType/UnionTypeTest.php index 78afde25..e491cd06 100644 --- a/test/Unit/TypeSystem/Type/UnionType/UnionTypeTest.php +++ b/test/Unit/TypeSystem/Type/UnionType/UnionTypeTest.php @@ -129,9 +129,9 @@ public function isNullableOnNullableString(): void $this->assertInstanceOf(UnionType::class, $unionType); - $this->assertTrue($unionType->isNullable()); + $this->assertTrue($unionType->containsNull()); - $nonNullables = $unionType->withoutNullable(); + $nonNullables = $unionType->withoutNull(); $this->assertTrue($nonNullables->is(StringType::get())); } @@ -145,9 +145,9 @@ public function isNullableWithMultipleItems(): void $this->assertInstanceOf(UnionType::class, $unionType); - $this->assertTrue($unionType->isNullable()); + $this->assertTrue($unionType->containsNull()); - $nonNullables = $unionType->withoutNullable(); + $nonNullables = $unionType->withoutNull(); $this->assertTrue($nonNullables->is(UnionType::of(StringType::get(), NumberType::get()))); } @@ -161,9 +161,9 @@ public function isNullableOnNonNullableUnion(): void $this->assertInstanceOf(UnionType::class, $unionType); - $this->assertFalse($unionType->isNullable()); + $this->assertFalse($unionType->containsNull()); - $nonNullables = $unionType->withoutNullable(); + $nonNullables = $unionType->withoutNull(); $this->assertTrue($nonNullables->is($unionType)); } From 8a34ee1d5f3824fa6641059fe1f2ac8e723dfbf7 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 22 Apr 2023 00:00:05 +0200 Subject: [PATCH 08/28] TASK: Introduce TernaryBranchScope --- .../TernaryOperationTypeResolver.php | 45 ++++++++----------- ...hallowScope.php => TernaryBranchScope.php} | 17 ++++--- 2 files changed, 31 insertions(+), 31 deletions(-) rename src/TypeSystem/Scope/ShallowScope/{ShallowScope.php => TernaryBranchScope.php} (65%) diff --git a/src/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolver.php b/src/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolver.php index f2b4242b..ce11f79b 100644 --- a/src/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolver.php +++ b/src/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolver.php @@ -23,12 +23,10 @@ namespace PackageFactory\ComponentEngine\TypeSystem\Resolver\TernaryOperation; use PackageFactory\ComponentEngine\Parser\Ast\BooleanLiteralNode; -use PackageFactory\ComponentEngine\Parser\Ast\IdentifierNode; use PackageFactory\ComponentEngine\Parser\Ast\TernaryOperationNode; use PackageFactory\ComponentEngine\TypeSystem\Resolver\Expression\ExpressionTypeResolver; -use PackageFactory\ComponentEngine\TypeSystem\Scope\ShallowScope\ShallowScope; +use PackageFactory\ComponentEngine\TypeSystem\Scope\ShallowScope\TernaryBranchScope; use PackageFactory\ComponentEngine\TypeSystem\ScopeInterface; -use PackageFactory\ComponentEngine\TypeSystem\Type\NullType\NullType; use PackageFactory\ComponentEngine\TypeSystem\Type\UnionType\UnionType; use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; @@ -54,32 +52,27 @@ public function resolveTypeOf(TernaryOperationNode $ternaryOperationNode): TypeI : $expressionTypeResolver->resolveTypeOf($ternaryOperationNode->false); } - if ($conditionNode->root instanceof IdentifierNode && $conditionType instanceof UnionType && $conditionType->containsNull()) { - $trueExpressionTypeResolver = new ExpressionTypeResolver( - scope: new ShallowScope( - $conditionNode->root->value, - $conditionType->withoutNull(), - $this->scope - ) - ); - - $falseExpressionTypeResolver = new ExpressionTypeResolver( - scope: new ShallowScope( - $conditionNode->root->value, - NullType::get(), - $this->scope - ) - ); + $trueExpressionTypeResolver = new ExpressionTypeResolver( + scope: new TernaryBranchScope( + $ternaryOperationNode->condition, + $conditionType, + true, + $this->scope + ) + ); - return UnionType::of( - $trueExpressionTypeResolver->resolveTypeOf($ternaryOperationNode->true), - $falseExpressionTypeResolver->resolveTypeOf($ternaryOperationNode->false) - ); - } + $falseExpressionTypeResolver = new ExpressionTypeResolver( + scope: new TernaryBranchScope( + $ternaryOperationNode->condition, + $conditionType, + false, + $this->scope + ) + ); return UnionType::of( - $expressionTypeResolver->resolveTypeOf($ternaryOperationNode->true), - $expressionTypeResolver->resolveTypeOf($ternaryOperationNode->false) + $trueExpressionTypeResolver->resolveTypeOf($ternaryOperationNode->true), + $falseExpressionTypeResolver->resolveTypeOf($ternaryOperationNode->false) ); } } diff --git a/src/TypeSystem/Scope/ShallowScope/ShallowScope.php b/src/TypeSystem/Scope/ShallowScope/TernaryBranchScope.php similarity index 65% rename from src/TypeSystem/Scope/ShallowScope/ShallowScope.php rename to src/TypeSystem/Scope/ShallowScope/TernaryBranchScope.php index 0c625d98..ce7b8ebc 100644 --- a/src/TypeSystem/Scope/ShallowScope/ShallowScope.php +++ b/src/TypeSystem/Scope/ShallowScope/TernaryBranchScope.php @@ -22,23 +22,30 @@ namespace PackageFactory\ComponentEngine\TypeSystem\Scope\ShallowScope; +use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNode; +use PackageFactory\ComponentEngine\Parser\Ast\IdentifierNode; use PackageFactory\ComponentEngine\Parser\Ast\TypeReferenceNode; use PackageFactory\ComponentEngine\TypeSystem\ScopeInterface; +use PackageFactory\ComponentEngine\TypeSystem\Type\NullType\NullType; +use PackageFactory\ComponentEngine\TypeSystem\Type\UnionType\UnionType; use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; -final class ShallowScope implements ScopeInterface +final class TernaryBranchScope implements ScopeInterface { public function __construct( - private readonly string $overriddenName, - private readonly TypeInterface $overriddenType, + private readonly ExpressionNode $conditionNode, + private readonly TypeInterface $conditionType, + private readonly bool $isBranchLeft, private readonly ScopeInterface $parentScope ) { } public function lookupTypeFor(string $name): ?TypeInterface { - if ($this->overriddenName === $name) { - return $this->overriddenType; + if ($this->conditionNode->root instanceof IdentifierNode && $this->conditionNode->root->value === $name) { + if ($this->conditionType instanceof UnionType && $this->conditionType->containsNull()) { + return $this->isBranchLeft ? $this->conditionType->withoutNull() : NullType::get(); + } } return $this->parentScope->lookupTypeFor($name); From 1b476e569278b7d6ab58adcf0697b5c9b10998ad Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 22 Apr 2023 00:26:58 +0200 Subject: [PATCH 09/28] TASK: Type inference for null comparison in ternary --- .../TernaryOperationTypeResolver.php | 5 ++- .../Scope/ShallowScope/TernaryBranchScope.php | 32 ++++++++++++++++--- .../TernaryOperationTypeResolverTest.php | 14 ++++++++ 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/src/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolver.php b/src/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolver.php index ce11f79b..336fac52 100644 --- a/src/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolver.php +++ b/src/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolver.php @@ -44,7 +44,8 @@ public function resolveTypeOf(TernaryOperationNode $ternaryOperationNode): TypeI ); $conditionNode = $ternaryOperationNode->condition; - $conditionType = $expressionTypeResolver->resolveTypeOf($conditionNode); + // @todo for eager type checks? + $expressionTypeResolver->resolveTypeOf($conditionNode); if ($conditionNode->root instanceof BooleanLiteralNode) { return $conditionNode->root->value @@ -55,7 +56,6 @@ public function resolveTypeOf(TernaryOperationNode $ternaryOperationNode): TypeI $trueExpressionTypeResolver = new ExpressionTypeResolver( scope: new TernaryBranchScope( $ternaryOperationNode->condition, - $conditionType, true, $this->scope ) @@ -64,7 +64,6 @@ public function resolveTypeOf(TernaryOperationNode $ternaryOperationNode): TypeI $falseExpressionTypeResolver = new ExpressionTypeResolver( scope: new TernaryBranchScope( $ternaryOperationNode->condition, - $conditionType, false, $this->scope ) diff --git a/src/TypeSystem/Scope/ShallowScope/TernaryBranchScope.php b/src/TypeSystem/Scope/ShallowScope/TernaryBranchScope.php index ce7b8ebc..07df2d46 100644 --- a/src/TypeSystem/Scope/ShallowScope/TernaryBranchScope.php +++ b/src/TypeSystem/Scope/ShallowScope/TernaryBranchScope.php @@ -22,8 +22,11 @@ namespace PackageFactory\ComponentEngine\TypeSystem\Scope\ShallowScope; +use PackageFactory\ComponentEngine\Definition\BinaryOperator; +use PackageFactory\ComponentEngine\Parser\Ast\BinaryOperationNode; use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNode; use PackageFactory\ComponentEngine\Parser\Ast\IdentifierNode; +use PackageFactory\ComponentEngine\Parser\Ast\NullLiteralNode; use PackageFactory\ComponentEngine\Parser\Ast\TypeReferenceNode; use PackageFactory\ComponentEngine\TypeSystem\ScopeInterface; use PackageFactory\ComponentEngine\TypeSystem\Type\NullType\NullType; @@ -34,7 +37,6 @@ final class TernaryBranchScope implements ScopeInterface { public function __construct( private readonly ExpressionNode $conditionNode, - private readonly TypeInterface $conditionType, private readonly bool $isBranchLeft, private readonly ScopeInterface $parentScope ) { @@ -42,13 +44,35 @@ public function __construct( public function lookupTypeFor(string $name): ?TypeInterface { + $type = $this->parentScope->lookupTypeFor($name); + + if (!$type instanceof UnionType || !$type->containsNull()) { + return $type; + } + if ($this->conditionNode->root instanceof IdentifierNode && $this->conditionNode->root->value === $name) { - if ($this->conditionType instanceof UnionType && $this->conditionType->containsNull()) { - return $this->isBranchLeft ? $this->conditionType->withoutNull() : NullType::get(); + return $this->isBranchLeft ? $type->withoutNull() : NullType::get(); + } + + if (($binaryOperationNode = $this->conditionNode->root) instanceof BinaryOperationNode) { + foreach ($binaryOperationNode->operands as $operand) { + if (!$operand->root instanceof NullLiteralNode + && !($operand->root instanceof IdentifierNode && $operand->root->value === $name) + ) { + return $type; + } + } + + if ($binaryOperationNode->operator === BinaryOperator::EQUAL) { + return $this->isBranchLeft ? NullType::get() : $type->withoutNull(); + } + + if ($binaryOperationNode->operator === BinaryOperator::NOT_EQUAL) { + return $this->isBranchLeft ? $type->withoutNull() : NullType::get(); } } - return $this->parentScope->lookupTypeFor($name); + return $type; } public function resolveTypeReference(TypeReferenceNode $typeReferenceNode): TypeInterface diff --git a/test/Unit/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolverTest.php b/test/Unit/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolverTest.php index 14aab685..64fd67a2 100644 --- a/test/Unit/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolverTest.php +++ b/test/Unit/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolverTest.php @@ -51,6 +51,20 @@ public function ternaryOperationExamples(): array 'nullableString ? nullableString : "fallback"' => [ 'nullableString ? nullableString : "fallback"', StringType::get() ], + 'nullableString === null ? "" : nullableString' => [ + 'nullableString === null ? "" : nullableString', StringType::get() + ], + // Tue es oder tue es nicht. Es gibt kein Versuchen. + 'null === nullableString ? "" : nullableString' => [ + 'null === nullableString ? "" : nullableString', StringType::get() + ], + 'nullableString !== null ? nullableString : ""' => [ + 'nullableString !== null ? nullableString : ""', StringType::get() + ], + // Patience you must have my young Padawan. + 'null !== nullableString ? nullableString : ""' => [ + 'null !== nullableString ? nullableString : ""', StringType::get() + ], 'nullableString ? null : nullableString' => [ 'nullableString ? null : nullableString', NullType::get() ], From 57ee20cad701a1f8518104a458b7eb20f6af3112 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 22 Apr 2023 00:59:08 +0200 Subject: [PATCH 10/28] TASK: UnionType RequiresAtLeastOneMember --- src/TypeSystem/Type/UnionType/UnionType.php | 11 +++-------- test/Unit/TypeSystem/Type/UnionType/UnionTypeTest.php | 10 ++++++++++ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/TypeSystem/Type/UnionType/UnionType.php b/src/TypeSystem/Type/UnionType/UnionType.php index 8dd48cd7..d768a855 100644 --- a/src/TypeSystem/Type/UnionType/UnionType.php +++ b/src/TypeSystem/Type/UnionType/UnionType.php @@ -37,16 +37,14 @@ final class UnionType implements TypeInterface, \IteratorAggregate, \Countable private function __construct(TypeInterface ...$members) { - if (count($members) < 1) { - throw new \Exception('UnionType can only hold more than one different members'); - } + assert(count($members) > 1, 'UnionType must hold at least two different members'); $this->members = $members; } - public static function of(TypeInterface ...$members): TypeInterface + public static function of(TypeInterface $firstMember, TypeInterface ...$members): TypeInterface { $uniqueMembers = []; - foreach ($members as $member) { + foreach ([$firstMember, ...$members] as $member) { foreach ($uniqueMembers as $uniqueMember) { if ($member->is($uniqueMember)) { continue 2; @@ -82,9 +80,6 @@ public function withoutNull(): TypeInterface } $nonNullMembers[] = $member; } - if (count($nonNullMembers) === 1) { - return $nonNullMembers[0]; - } return self::of(...$nonNullMembers); } diff --git a/test/Unit/TypeSystem/Type/UnionType/UnionTypeTest.php b/test/Unit/TypeSystem/Type/UnionType/UnionTypeTest.php index e491cd06..b5aec39f 100644 --- a/test/Unit/TypeSystem/Type/UnionType/UnionTypeTest.php +++ b/test/Unit/TypeSystem/Type/UnionType/UnionTypeTest.php @@ -30,6 +30,16 @@ final class UnionTypeTest extends TestCase { + /** + * @test + */ + public function unionRequiresAtLeastOneMember(): void + { + $this->expectException(\TypeError::class); + /** @phpstan-ignore-next-line */ + UnionType::of(); + } + /** * @test */ From 3487bd99cbf08a1446278be2dbaa9f3d1b22aeb4 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 22 Apr 2023 01:25:04 +0200 Subject: [PATCH 11/28] TASK: TernaryBranchScope introduce static factories and dont throw booleans around > "it must be true though" https://www.youtube.com/watch?v=7EmboKQH8lM&t=4407s --- .../TernaryOperationTypeResolver.php | 6 ++---- .../Scope/ShallowScope/TernaryBranchScope.php | 20 ++++++++++++++----- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolver.php b/src/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolver.php index 336fac52..7d9f15da 100644 --- a/src/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolver.php +++ b/src/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolver.php @@ -54,17 +54,15 @@ public function resolveTypeOf(TernaryOperationNode $ternaryOperationNode): TypeI } $trueExpressionTypeResolver = new ExpressionTypeResolver( - scope: new TernaryBranchScope( + scope: TernaryBranchScope::forTrueBranch( $ternaryOperationNode->condition, - true, $this->scope ) ); $falseExpressionTypeResolver = new ExpressionTypeResolver( - scope: new TernaryBranchScope( + scope: TernaryBranchScope::forFalseBranch( $ternaryOperationNode->condition, - false, $this->scope ) ); diff --git a/src/TypeSystem/Scope/ShallowScope/TernaryBranchScope.php b/src/TypeSystem/Scope/ShallowScope/TernaryBranchScope.php index 07df2d46..743546af 100644 --- a/src/TypeSystem/Scope/ShallowScope/TernaryBranchScope.php +++ b/src/TypeSystem/Scope/ShallowScope/TernaryBranchScope.php @@ -35,13 +35,23 @@ final class TernaryBranchScope implements ScopeInterface { - public function __construct( + private function __construct( private readonly ExpressionNode $conditionNode, - private readonly bool $isBranchLeft, + private readonly bool $isBranchTrue, private readonly ScopeInterface $parentScope ) { } + public static function forTrueBranch(ExpressionNode $conditionNode, ScopeInterface $parentScope): self + { + return new self(conditionNode: $conditionNode, isBranchTrue: true, parentScope: $parentScope); + } + + public static function forFalseBranch(ExpressionNode $conditionNode, ScopeInterface $parentScope): self + { + return new self(conditionNode: $conditionNode, isBranchTrue: false, parentScope: $parentScope); + } + public function lookupTypeFor(string $name): ?TypeInterface { $type = $this->parentScope->lookupTypeFor($name); @@ -51,7 +61,7 @@ public function lookupTypeFor(string $name): ?TypeInterface } if ($this->conditionNode->root instanceof IdentifierNode && $this->conditionNode->root->value === $name) { - return $this->isBranchLeft ? $type->withoutNull() : NullType::get(); + return $this->isBranchTrue ? $type->withoutNull() : NullType::get(); } if (($binaryOperationNode = $this->conditionNode->root) instanceof BinaryOperationNode) { @@ -64,11 +74,11 @@ public function lookupTypeFor(string $name): ?TypeInterface } if ($binaryOperationNode->operator === BinaryOperator::EQUAL) { - return $this->isBranchLeft ? NullType::get() : $type->withoutNull(); + return $this->isBranchTrue ? NullType::get() : $type->withoutNull(); } if ($binaryOperationNode->operator === BinaryOperator::NOT_EQUAL) { - return $this->isBranchLeft ? $type->withoutNull() : NullType::get(); + return $this->isBranchTrue ? $type->withoutNull() : NullType::get(); } } From 1348ac1a1ebdcb67dd6c9ea6445e6239ea196acb Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 22 Apr 2023 11:16:21 +0200 Subject: [PATCH 12/28] TASK: Make type inference in TernaryBranchScope more explicit --- .../Scope/ShallowScope/TernaryBranchScope.php | 25 +++++++++++++------ .../TernaryOperationTypeResolverTest.php | 1 - 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/TypeSystem/Scope/ShallowScope/TernaryBranchScope.php b/src/TypeSystem/Scope/ShallowScope/TernaryBranchScope.php index 743546af..0025706d 100644 --- a/src/TypeSystem/Scope/ShallowScope/TernaryBranchScope.php +++ b/src/TypeSystem/Scope/ShallowScope/TernaryBranchScope.php @@ -61,22 +61,31 @@ public function lookupTypeFor(string $name): ?TypeInterface } if ($this->conditionNode->root instanceof IdentifierNode && $this->conditionNode->root->value === $name) { + // case `nullableString ? "nullableString is not null" : "nullableString is null"` return $this->isBranchTrue ? $type->withoutNull() : NullType::get(); } if (($binaryOperationNode = $this->conditionNode->root) instanceof BinaryOperationNode) { - foreach ($binaryOperationNode->operands as $operand) { - if (!$operand->root instanceof NullLiteralNode - && !($operand->root instanceof IdentifierNode && $operand->root->value === $name) - ) { - return $type; - } + // cases + // `nullableString === null ? "nullableString is null" : "nullableString is not null"` + // `nullableString !== null ? "nullableString is not null" : "nullableString is null"` + if (count($binaryOperationNode->operands->rest) !== 1) { + return $type; + } + $first = $binaryOperationNode->operands->first; + $second = $binaryOperationNode->operands->rest[0]; + // case `nullableString === null` + $isFirstToBeLookedUpName = $first->root instanceof IdentifierNode && $first->root->value === $name; + $isFirstComparedToNull = $isFirstToBeLookedUpName && $second->root instanceof NullLiteralNode; + // yodas case `null === nullableString` + $isSecondToBeLookedUpName = $second->root instanceof IdentifierNode && $second->root->value === $name; + $isSecondComparedToNull = $first->root instanceof NullLiteralNode && $isSecondToBeLookedUpName; + if (!$isFirstComparedToNull && !$isSecondComparedToNull) { + return $type; } - if ($binaryOperationNode->operator === BinaryOperator::EQUAL) { return $this->isBranchTrue ? NullType::get() : $type->withoutNull(); } - if ($binaryOperationNode->operator === BinaryOperator::NOT_EQUAL) { return $this->isBranchTrue ? $type->withoutNull() : NullType::get(); } diff --git a/test/Unit/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolverTest.php b/test/Unit/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolverTest.php index 64fd67a2..0dc64d80 100644 --- a/test/Unit/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolverTest.php +++ b/test/Unit/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolverTest.php @@ -54,7 +54,6 @@ public function ternaryOperationExamples(): array 'nullableString === null ? "" : nullableString' => [ 'nullableString === null ? "" : nullableString', StringType::get() ], - // Tue es oder tue es nicht. Es gibt kein Versuchen. 'null === nullableString ? "" : nullableString' => [ 'null === nullableString ? "" : nullableString', StringType::get() ], From 492e05c97f80f7ca71a2a43df641b486cbd11d39 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 22 Apr 2023 11:36:13 +0200 Subject: [PATCH 13/28] TASK: Adjust naming of $nonNullable to $typeWithoutNull --- .../TypeReference/TypeReferenceTranspiler.php | 9 +++------ .../Type/UnionType/UnionTypeTest.php | 18 +++++++++--------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/src/Target/Php/Transpiler/TypeReference/TypeReferenceTranspiler.php b/src/Target/Php/Transpiler/TypeReference/TypeReferenceTranspiler.php index f205106a..d0044a17 100644 --- a/src/Target/Php/Transpiler/TypeReference/TypeReferenceTranspiler.php +++ b/src/Target/Php/Transpiler/TypeReference/TypeReferenceTranspiler.php @@ -54,12 +54,9 @@ public function transpile(TypeReferenceNode $typeReferenceNode): string private function transpileUnionType(UnionType $unionType, TypeReferenceNode $typeReferenceNode): string { - if ($unionType->containsNull()) { - $nonNullable = $unionType->withoutNull(); - if ($nonNullable instanceof UnionType) { - throw new \Exception('@TODO Transpilation of nullable union types with more non null members is not implemented'); - } - return $this->transpileNullableType($nonNullable, $typeReferenceNode); + if (count($unionType) === 2 && $unionType->containsNull()) { + $typeWithoutNull = $unionType->withoutNull(); + return $this->transpileNullableType($typeWithoutNull, $typeReferenceNode); } throw new \Exception('@TODO Transpilation of complex union types is not implemented'); diff --git a/test/Unit/TypeSystem/Type/UnionType/UnionTypeTest.php b/test/Unit/TypeSystem/Type/UnionType/UnionTypeTest.php index b5aec39f..9f628ae7 100644 --- a/test/Unit/TypeSystem/Type/UnionType/UnionTypeTest.php +++ b/test/Unit/TypeSystem/Type/UnionType/UnionTypeTest.php @@ -133,7 +133,7 @@ public function isReturnsFalseIfGivenTypeIsNotCongruent(): void /** * @test */ - public function isNullableOnNullableString(): void + public function containsNullOnNullableString(): void { $unionType = UnionType::of(StringType::get(), NullType::get()); @@ -141,15 +141,15 @@ public function isNullableOnNullableString(): void $this->assertTrue($unionType->containsNull()); - $nonNullables = $unionType->withoutNull(); + $withoutNull = $unionType->withoutNull(); - $this->assertTrue($nonNullables->is(StringType::get())); + $this->assertTrue($withoutNull->is(StringType::get())); } /** * @test */ - public function isNullableWithMultipleItems(): void + public function containsNullWithMultipleMembers(): void { $unionType = UnionType::of(StringType::get(), NumberType::get(), NullType::get()); @@ -157,15 +157,15 @@ public function isNullableWithMultipleItems(): void $this->assertTrue($unionType->containsNull()); - $nonNullables = $unionType->withoutNull(); + $withoutNull = $unionType->withoutNull(); - $this->assertTrue($nonNullables->is(UnionType::of(StringType::get(), NumberType::get()))); + $this->assertTrue($withoutNull->is(UnionType::of(StringType::get(), NumberType::get()))); } /** * @test */ - public function isNullableOnNonNullableUnion(): void + public function withoutNullOnUnionWithoutNull(): void { $unionType = UnionType::of(StringType::get(), NumberType::get()); @@ -173,8 +173,8 @@ public function isNullableOnNonNullableUnion(): void $this->assertFalse($unionType->containsNull()); - $nonNullables = $unionType->withoutNull(); + $withoutNull = $unionType->withoutNull(); - $this->assertTrue($nonNullables->is($unionType)); + $this->assertTrue($withoutNull->is($unionType)); } } From 0c7061f21416ed7f7be103bb0f4f9ea5af007756 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 22 Apr 2023 11:37:41 +0200 Subject: [PATCH 14/28] TASK: Solve #7 rudimentary This might be error-prone but why not ^^ --- .../Transpiler/TypeReference/TypeReferenceTranspiler.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Target/Php/Transpiler/TypeReference/TypeReferenceTranspiler.php b/src/Target/Php/Transpiler/TypeReference/TypeReferenceTranspiler.php index d0044a17..d3bed4f8 100644 --- a/src/Target/Php/Transpiler/TypeReference/TypeReferenceTranspiler.php +++ b/src/Target/Php/Transpiler/TypeReference/TypeReferenceTranspiler.php @@ -77,11 +77,9 @@ private function transpileNonUnionType(TypeInterface $type, TypeReferenceNode $t }; } - private function transpileNullableType(TypeInterface $type, TypeReferenceNode $typeReferenceNode): string + private function transpileNullableType(TypeInterface $typeWithoutNull, TypeReferenceNode $typeReferenceNode): string { - if ($type->is(NumberType::get())) { - return 'null|int|float'; - } - return '?' . $this->transpileNonUnionType($type, $typeReferenceNode); + $phpTypeWithoutNull = $this->transpileNonUnionType($typeWithoutNull, $typeReferenceNode); + return (str_contains($phpTypeWithoutNull, '|') ? 'null|' : '?') . $phpTypeWithoutNull; } } From 22df4baa5b3adc7d5c7498b5653e8d306c0aa288 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 22 Apr 2023 21:04:10 +0200 Subject: [PATCH 15/28] TASK: Introduce TypeInferrer inspired by phpstan to support future advanced type inference --- src/TypeSystem/Inferrer/InferredTypes.php | 39 +++++++ src/TypeSystem/Inferrer/TypeInferrer.php | 107 ++++++++++++++++++ .../Inferrer/TypeInferrerContext.php | 40 +++++++ .../TernaryOperationTypeResolver.php | 26 ++--- .../Scope/ShallowScope/TernaryBranchScope.php | 101 ----------------- .../TernaryBranchScope/TernaryBranchScope.php | 66 +++++++++++ 6 files changed, 261 insertions(+), 118 deletions(-) create mode 100644 src/TypeSystem/Inferrer/InferredTypes.php create mode 100644 src/TypeSystem/Inferrer/TypeInferrer.php create mode 100644 src/TypeSystem/Inferrer/TypeInferrerContext.php delete mode 100644 src/TypeSystem/Scope/ShallowScope/TernaryBranchScope.php create mode 100644 src/TypeSystem/Scope/TernaryBranchScope/TernaryBranchScope.php diff --git a/src/TypeSystem/Inferrer/InferredTypes.php b/src/TypeSystem/Inferrer/InferredTypes.php new file mode 100644 index 00000000..946e202a --- /dev/null +++ b/src/TypeSystem/Inferrer/InferredTypes.php @@ -0,0 +1,39 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\TypeSystem\Inferrer; + +use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; + +class InferredTypes +{ + /** + * @var TypeInterface[] + */ + public readonly array $types; + + public function __construct( + TypeInterface ...$types + ) { + $this->types = $types; + } +} diff --git a/src/TypeSystem/Inferrer/TypeInferrer.php b/src/TypeSystem/Inferrer/TypeInferrer.php new file mode 100644 index 00000000..5bbfda61 --- /dev/null +++ b/src/TypeSystem/Inferrer/TypeInferrer.php @@ -0,0 +1,107 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\TypeSystem\Inferrer; + +use PackageFactory\ComponentEngine\Definition\BinaryOperator; +use PackageFactory\ComponentEngine\Parser\Ast\BinaryOperationNode; +use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNode; +use PackageFactory\ComponentEngine\Parser\Ast\IdentifierNode; +use PackageFactory\ComponentEngine\Parser\Ast\NullLiteralNode; +use PackageFactory\ComponentEngine\TypeSystem\ScopeInterface; +use PackageFactory\ComponentEngine\TypeSystem\Type\NullType\NullType; +use PackageFactory\ComponentEngine\TypeSystem\Type\UnionType\UnionType; + +/** + * This class handles the analysis of identifier types that are used in a condition + * and based on the requested branch: truthy or falsy, will predict the types a variable will have in the respective branch + * so it matches the expected runtime behaviour + * + * For example given this expression: `nullableString ? "nullableString is not null" : "nullableString is null"` based on the condition `nullableString` + * It will infer that in the truthy context nullableString is a string while in the falsy context it will infer that it is a null + * + * The structure is partially inspired by phpstan + * https://github.com/phpstan/phpstan-src/blob/07bb4aa2d5e39dafa78f56c5df132c763c2d1b67/src/Analyser/TypeSpecifier.php#L111 + */ +class TypeInferrer +{ + public function __construct( + private readonly ScopeInterface $scope + ) { + } + + public function inferTypesInCondition(ExpressionNode $conditionNode, TypeInferrerContext $context): InferredTypes + { + if ($conditionNode->root instanceof IdentifierNode) { + $type = $this->scope->lookupTypeFor($conditionNode->root->value); + // case `nullableString ? "nullableString is not null" : "nullableString is null"` + if (!$type instanceof UnionType || !$type->containsNull()) { + return new InferredTypes(); + } + + return new InferredTypes( + ...[$conditionNode->root->value => $context->isTrue() ? $type->withoutNull() : NullType::get()] + ); + } + + if (($binaryOperationNode = $conditionNode->root) instanceof BinaryOperationNode) { + // cases + // `nullableString === null ? "nullableString is null" : "nullableString is not null"` + // `nullableString !== null ? "nullableString is not null" : "nullableString is null"` + if (count($binaryOperationNode->operands->rest) !== 1) { + return new InferredTypes(); + } + $first = $binaryOperationNode->operands->first; + $second = $binaryOperationNode->operands->rest[0]; + + $comparedIdentifierValueToNull = match (true) { + // case `nullableString === null` + $first->root instanceof IdentifierNode && $second->root instanceof NullLiteralNode => $first->root->value, + // yodas case `null === nullableString` + $first->root instanceof NullLiteralNode && $second->root instanceof IdentifierNode => $second->root->value, + default => null + }; + + if ($comparedIdentifierValueToNull === null) { + return new InferredTypes(); + } + + $type = $this->scope->lookupTypeFor($comparedIdentifierValueToNull); + if (!$type instanceof UnionType || !$type->containsNull()) { + return new InferredTypes(); + } + + if ($binaryOperationNode->operator === BinaryOperator::EQUAL) { + return new InferredTypes( + ...[$comparedIdentifierValueToNull => $context->isTrue() ? NullType::get() : $type->withoutNull()] + ); + } + if ($binaryOperationNode->operator === BinaryOperator::NOT_EQUAL) { + return new InferredTypes( + ...[$comparedIdentifierValueToNull => $context->isTrue() ? $type->withoutNull() : NullType::get()] + ); + } + } + + return new InferredTypes(); + } +} diff --git a/src/TypeSystem/Inferrer/TypeInferrerContext.php b/src/TypeSystem/Inferrer/TypeInferrerContext.php new file mode 100644 index 00000000..61f44a6d --- /dev/null +++ b/src/TypeSystem/Inferrer/TypeInferrerContext.php @@ -0,0 +1,40 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\TypeSystem\Inferrer; + +enum TypeInferrerContext +{ + case TRUTHY; + + case FALSY; + + public function isTrue(): bool + { + return $this === self::TRUTHY; + } + + public function isFalse(): bool + { + return $this === self::FALSY; + } +} diff --git a/src/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolver.php b/src/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolver.php index 7d9f15da..f2a662a0 100644 --- a/src/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolver.php +++ b/src/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolver.php @@ -25,7 +25,7 @@ use PackageFactory\ComponentEngine\Parser\Ast\BooleanLiteralNode; use PackageFactory\ComponentEngine\Parser\Ast\TernaryOperationNode; use PackageFactory\ComponentEngine\TypeSystem\Resolver\Expression\ExpressionTypeResolver; -use PackageFactory\ComponentEngine\TypeSystem\Scope\ShallowScope\TernaryBranchScope; +use PackageFactory\ComponentEngine\TypeSystem\Scope\TernaryBranchScope\TernaryBranchScope; use PackageFactory\ComponentEngine\TypeSystem\ScopeInterface; use PackageFactory\ComponentEngine\TypeSystem\Type\UnionType\UnionType; use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; @@ -39,34 +39,26 @@ public function __construct( public function resolveTypeOf(TernaryOperationNode $ternaryOperationNode): TypeInterface { - $expressionTypeResolver = new ExpressionTypeResolver( - scope: $this->scope - ); - $conditionNode = $ternaryOperationNode->condition; - - // @todo for eager type checks? - $expressionTypeResolver->resolveTypeOf($conditionNode); - - if ($conditionNode->root instanceof BooleanLiteralNode) { - return $conditionNode->root->value - ? $expressionTypeResolver->resolveTypeOf($ternaryOperationNode->true) - : $expressionTypeResolver->resolveTypeOf($ternaryOperationNode->false); - } - $trueExpressionTypeResolver = new ExpressionTypeResolver( - scope: TernaryBranchScope::forTrueBranch( + scope: TernaryBranchScope::forTruthyBranch( $ternaryOperationNode->condition, $this->scope ) ); $falseExpressionTypeResolver = new ExpressionTypeResolver( - scope: TernaryBranchScope::forFalseBranch( + scope: TernaryBranchScope::forFalsyBranch( $ternaryOperationNode->condition, $this->scope ) ); + if ($ternaryOperationNode->condition->root instanceof BooleanLiteralNode) { + return $ternaryOperationNode->condition->root->value + ? $trueExpressionTypeResolver->resolveTypeOf($ternaryOperationNode->true) + : $falseExpressionTypeResolver->resolveTypeOf($ternaryOperationNode->false); + } + return UnionType::of( $trueExpressionTypeResolver->resolveTypeOf($ternaryOperationNode->true), $falseExpressionTypeResolver->resolveTypeOf($ternaryOperationNode->false) diff --git a/src/TypeSystem/Scope/ShallowScope/TernaryBranchScope.php b/src/TypeSystem/Scope/ShallowScope/TernaryBranchScope.php deleted file mode 100644 index 0025706d..00000000 --- a/src/TypeSystem/Scope/ShallowScope/TernaryBranchScope.php +++ /dev/null @@ -1,101 +0,0 @@ -. - */ - -declare(strict_types=1); - -namespace PackageFactory\ComponentEngine\TypeSystem\Scope\ShallowScope; - -use PackageFactory\ComponentEngine\Definition\BinaryOperator; -use PackageFactory\ComponentEngine\Parser\Ast\BinaryOperationNode; -use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNode; -use PackageFactory\ComponentEngine\Parser\Ast\IdentifierNode; -use PackageFactory\ComponentEngine\Parser\Ast\NullLiteralNode; -use PackageFactory\ComponentEngine\Parser\Ast\TypeReferenceNode; -use PackageFactory\ComponentEngine\TypeSystem\ScopeInterface; -use PackageFactory\ComponentEngine\TypeSystem\Type\NullType\NullType; -use PackageFactory\ComponentEngine\TypeSystem\Type\UnionType\UnionType; -use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; - -final class TernaryBranchScope implements ScopeInterface -{ - private function __construct( - private readonly ExpressionNode $conditionNode, - private readonly bool $isBranchTrue, - private readonly ScopeInterface $parentScope - ) { - } - - public static function forTrueBranch(ExpressionNode $conditionNode, ScopeInterface $parentScope): self - { - return new self(conditionNode: $conditionNode, isBranchTrue: true, parentScope: $parentScope); - } - - public static function forFalseBranch(ExpressionNode $conditionNode, ScopeInterface $parentScope): self - { - return new self(conditionNode: $conditionNode, isBranchTrue: false, parentScope: $parentScope); - } - - public function lookupTypeFor(string $name): ?TypeInterface - { - $type = $this->parentScope->lookupTypeFor($name); - - if (!$type instanceof UnionType || !$type->containsNull()) { - return $type; - } - - if ($this->conditionNode->root instanceof IdentifierNode && $this->conditionNode->root->value === $name) { - // case `nullableString ? "nullableString is not null" : "nullableString is null"` - return $this->isBranchTrue ? $type->withoutNull() : NullType::get(); - } - - if (($binaryOperationNode = $this->conditionNode->root) instanceof BinaryOperationNode) { - // cases - // `nullableString === null ? "nullableString is null" : "nullableString is not null"` - // `nullableString !== null ? "nullableString is not null" : "nullableString is null"` - if (count($binaryOperationNode->operands->rest) !== 1) { - return $type; - } - $first = $binaryOperationNode->operands->first; - $second = $binaryOperationNode->operands->rest[0]; - // case `nullableString === null` - $isFirstToBeLookedUpName = $first->root instanceof IdentifierNode && $first->root->value === $name; - $isFirstComparedToNull = $isFirstToBeLookedUpName && $second->root instanceof NullLiteralNode; - // yodas case `null === nullableString` - $isSecondToBeLookedUpName = $second->root instanceof IdentifierNode && $second->root->value === $name; - $isSecondComparedToNull = $first->root instanceof NullLiteralNode && $isSecondToBeLookedUpName; - if (!$isFirstComparedToNull && !$isSecondComparedToNull) { - return $type; - } - if ($binaryOperationNode->operator === BinaryOperator::EQUAL) { - return $this->isBranchTrue ? NullType::get() : $type->withoutNull(); - } - if ($binaryOperationNode->operator === BinaryOperator::NOT_EQUAL) { - return $this->isBranchTrue ? $type->withoutNull() : NullType::get(); - } - } - - return $type; - } - - public function resolveTypeReference(TypeReferenceNode $typeReferenceNode): TypeInterface - { - return $this->parentScope->resolveTypeReference($typeReferenceNode); - } -} diff --git a/src/TypeSystem/Scope/TernaryBranchScope/TernaryBranchScope.php b/src/TypeSystem/Scope/TernaryBranchScope/TernaryBranchScope.php new file mode 100644 index 00000000..6987144d --- /dev/null +++ b/src/TypeSystem/Scope/TernaryBranchScope/TernaryBranchScope.php @@ -0,0 +1,66 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\TypeSystem\Scope\TernaryBranchScope; + +use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNode; +use PackageFactory\ComponentEngine\Parser\Ast\TypeReferenceNode; +use PackageFactory\ComponentEngine\TypeSystem\Inferrer\InferredTypes; +use PackageFactory\ComponentEngine\TypeSystem\Inferrer\TypeInferrer; +use PackageFactory\ComponentEngine\TypeSystem\Inferrer\TypeInferrerContext; +use PackageFactory\ComponentEngine\TypeSystem\ScopeInterface; +use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; + +final class TernaryBranchScope implements ScopeInterface +{ + private function __construct( + private readonly InferredTypes $inferredTypes, + private readonly ScopeInterface $parentScope + ) { + } + + public static function forTruthyBranch(ExpressionNode $conditionNode, ScopeInterface $parentScope): self + { + return new self( + (new TypeInferrer($parentScope))->inferTypesInCondition($conditionNode, TypeInferrerContext::TRUTHY), + $parentScope + ); + } + + public static function forFalsyBranch(ExpressionNode $conditionNode, ScopeInterface $parentScope): self + { + return new self( + (new TypeInferrer($parentScope))->inferTypesInCondition($conditionNode, TypeInferrerContext::FALSY), + $parentScope + ); + } + + public function lookupTypeFor(string $name): ?TypeInterface + { + return $this->inferredTypes->types[$name] ?? $this->parentScope->lookupTypeFor($name); + } + + public function resolveTypeReference(TypeReferenceNode $typeReferenceNode): TypeInterface + { + return $this->parentScope->resolveTypeReference($typeReferenceNode); + } +} From 0adb4c00c58e3a0a1de6d472cf166f438434d425 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 23 Apr 2023 11:11:13 +0200 Subject: [PATCH 16/28] TASK: Cleanup InferredTypes and extract duplicated logic to TypeInferrerContext --- src/TypeSystem/Inferrer/InferredTypes.php | 23 +++++++++++-- src/TypeSystem/Inferrer/TypeInferrer.php | 32 +++++++------------ .../Inferrer/TypeInferrerContext.php | 21 +++++++++--- .../TernaryBranchScope/TernaryBranchScope.php | 2 +- 4 files changed, 49 insertions(+), 29 deletions(-) diff --git a/src/TypeSystem/Inferrer/InferredTypes.php b/src/TypeSystem/Inferrer/InferredTypes.php index 946e202a..56df40ca 100644 --- a/src/TypeSystem/Inferrer/InferredTypes.php +++ b/src/TypeSystem/Inferrer/InferredTypes.php @@ -27,13 +27,30 @@ class InferredTypes { /** - * @var TypeInterface[] + * Map of identifierName to the corresponding inferred type + * @var array */ - public readonly array $types; + private readonly array $types; - public function __construct( + private function __construct( TypeInterface ...$types ) { + // @phpstan-ignore-next-line $this->types = $types; } + + public static function empty(): self + { + return new self(); + } + + public static function fromType(string $identifierName, TypeInterface $type): self + { + return new self(...[$identifierName => $type]); + } + + public function getType(string $identifierName): ?TypeInterface + { + return $this->types[$identifierName] ?? null; + } } diff --git a/src/TypeSystem/Inferrer/TypeInferrer.php b/src/TypeSystem/Inferrer/TypeInferrer.php index 5bbfda61..a4bcf1ac 100644 --- a/src/TypeSystem/Inferrer/TypeInferrer.php +++ b/src/TypeSystem/Inferrer/TypeInferrer.php @@ -28,8 +28,6 @@ use PackageFactory\ComponentEngine\Parser\Ast\IdentifierNode; use PackageFactory\ComponentEngine\Parser\Ast\NullLiteralNode; use PackageFactory\ComponentEngine\TypeSystem\ScopeInterface; -use PackageFactory\ComponentEngine\TypeSystem\Type\NullType\NullType; -use PackageFactory\ComponentEngine\TypeSystem\Type\UnionType\UnionType; /** * This class handles the analysis of identifier types that are used in a condition @@ -53,14 +51,11 @@ public function inferTypesInCondition(ExpressionNode $conditionNode, TypeInferre { if ($conditionNode->root instanceof IdentifierNode) { $type = $this->scope->lookupTypeFor($conditionNode->root->value); - // case `nullableString ? "nullableString is not null" : "nullableString is null"` - if (!$type instanceof UnionType || !$type->containsNull()) { - return new InferredTypes(); + if (!$type) { + return InferredTypes::empty(); } - - return new InferredTypes( - ...[$conditionNode->root->value => $context->isTrue() ? $type->withoutNull() : NullType::get()] - ); + // case `nullableString ? "nullableString is not null" : "nullableString is null"` + return InferredTypes::fromType($conditionNode->root->value, $context->narrowDownType($type)); } if (($binaryOperationNode = $conditionNode->root) instanceof BinaryOperationNode) { @@ -68,7 +63,7 @@ public function inferTypesInCondition(ExpressionNode $conditionNode, TypeInferre // `nullableString === null ? "nullableString is null" : "nullableString is not null"` // `nullableString !== null ? "nullableString is not null" : "nullableString is null"` if (count($binaryOperationNode->operands->rest) !== 1) { - return new InferredTypes(); + return InferredTypes::empty(); } $first = $binaryOperationNode->operands->first; $second = $binaryOperationNode->operands->rest[0]; @@ -82,26 +77,21 @@ public function inferTypesInCondition(ExpressionNode $conditionNode, TypeInferre }; if ($comparedIdentifierValueToNull === null) { - return new InferredTypes(); + return InferredTypes::empty(); } - $type = $this->scope->lookupTypeFor($comparedIdentifierValueToNull); - if (!$type instanceof UnionType || !$type->containsNull()) { - return new InferredTypes(); + if (!$type) { + return InferredTypes::empty(); } if ($binaryOperationNode->operator === BinaryOperator::EQUAL) { - return new InferredTypes( - ...[$comparedIdentifierValueToNull => $context->isTrue() ? NullType::get() : $type->withoutNull()] - ); + return InferredTypes::fromType($comparedIdentifierValueToNull, $context->negate()->narrowDownType($type)); } if ($binaryOperationNode->operator === BinaryOperator::NOT_EQUAL) { - return new InferredTypes( - ...[$comparedIdentifierValueToNull => $context->isTrue() ? $type->withoutNull() : NullType::get()] - ); + return InferredTypes::fromType($comparedIdentifierValueToNull, $context->narrowDownType($type)); } } - return new InferredTypes(); + return InferredTypes::empty(); } } diff --git a/src/TypeSystem/Inferrer/TypeInferrerContext.php b/src/TypeSystem/Inferrer/TypeInferrerContext.php index 61f44a6d..6c2d0066 100644 --- a/src/TypeSystem/Inferrer/TypeInferrerContext.php +++ b/src/TypeSystem/Inferrer/TypeInferrerContext.php @@ -22,19 +22,32 @@ namespace PackageFactory\ComponentEngine\TypeSystem\Inferrer; +use PackageFactory\ComponentEngine\TypeSystem\Type\NullType\NullType; +use PackageFactory\ComponentEngine\TypeSystem\Type\UnionType\UnionType; +use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; + enum TypeInferrerContext { case TRUTHY; case FALSY; - public function isTrue(): bool + public function negate(): self { - return $this === self::TRUTHY; + return match ($this) { + self::TRUTHY => self::FALSY, + self::FALSY => self::TRUTHY + }; } - public function isFalse(): bool + public function narrowDownType(TypeInterface $type): TypeInterface { - return $this === self::FALSY; + if (!$type instanceof UnionType || !$type->containsNull()) { + return $type; + } + return match ($this) { + self::TRUTHY => $type->withoutNull(), + self::FALSY => NullType::get() + }; } } diff --git a/src/TypeSystem/Scope/TernaryBranchScope/TernaryBranchScope.php b/src/TypeSystem/Scope/TernaryBranchScope/TernaryBranchScope.php index 6987144d..c3024920 100644 --- a/src/TypeSystem/Scope/TernaryBranchScope/TernaryBranchScope.php +++ b/src/TypeSystem/Scope/TernaryBranchScope/TernaryBranchScope.php @@ -56,7 +56,7 @@ public static function forFalsyBranch(ExpressionNode $conditionNode, ScopeInterf public function lookupTypeFor(string $name): ?TypeInterface { - return $this->inferredTypes->types[$name] ?? $this->parentScope->lookupTypeFor($name); + return $this->inferredTypes->getType($name) ?? $this->parentScope->lookupTypeFor($name); } public function resolveTypeReference(TypeReferenceNode $typeReferenceNode): TypeInterface From f61b89602d7af1591611768dd80d02f20d8410bf Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 23 Apr 2023 11:32:50 +0200 Subject: [PATCH 17/28] TASK: Remove `@phpstan-ignore-next-line` by asserting that an array is indeed associative (0 cost in production) Sadly we cant annotate the param in the constructor to tell psalm, that we only want to accept string keys. At least i have found no way --- src/TypeSystem/Inferrer/InferredTypes.php | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/TypeSystem/Inferrer/InferredTypes.php b/src/TypeSystem/Inferrer/InferredTypes.php index 56df40ca..bb106b9c 100644 --- a/src/TypeSystem/Inferrer/InferredTypes.php +++ b/src/TypeSystem/Inferrer/InferredTypes.php @@ -35,7 +35,7 @@ class InferredTypes private function __construct( TypeInterface ...$types ) { - // @phpstan-ignore-next-line + assert(self::isAssociativeArray($types), '$types must be an associative array'); $this->types = $types; } @@ -53,4 +53,20 @@ public function getType(string $identifierName): ?TypeInterface { return $this->types[$identifierName] ?? null; } + + /** + * @template T + * @param array $array + * @phpstan-assert-if-true array $array + */ + private static function isAssociativeArray(array $array): bool + { + foreach ($array as $key => $value) { + if (is_string($key)) { + continue; + } + return false; + } + return true; + } } From d835fa58254e915f2c1703b4d0ecee8f9df02d3f Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Wed, 26 Apr 2023 07:49:49 +0200 Subject: [PATCH 18/28] TASK: UnionType::getIterator use `yield from` --- src/TypeSystem/Type/UnionType/UnionType.php | 12 +++++++++--- .../Unit/TypeSystem/Type/UnionType/UnionTypeTest.php | 4 ++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/TypeSystem/Type/UnionType/UnionType.php b/src/TypeSystem/Type/UnionType/UnionType.php index d768a855..78371379 100644 --- a/src/TypeSystem/Type/UnionType/UnionType.php +++ b/src/TypeSystem/Type/UnionType/UnionType.php @@ -106,10 +106,16 @@ public function is(TypeInterface $other): bool } } - /** @return \ArrayIterator */ - public function getIterator(): \ArrayIterator + /** @return \Iterator */ + public function getIterator(): \Iterator { - return new \ArrayIterator($this->members); + yield from $this->members; + } + + /** @return array */ + public function toArray(): array + { + return $this->members; } public function count(): int diff --git a/test/Unit/TypeSystem/Type/UnionType/UnionTypeTest.php b/test/Unit/TypeSystem/Type/UnionType/UnionTypeTest.php index 9f628ae7..eb54784b 100644 --- a/test/Unit/TypeSystem/Type/UnionType/UnionTypeTest.php +++ b/test/Unit/TypeSystem/Type/UnionType/UnionTypeTest.php @@ -114,8 +114,8 @@ public function unionOnlyHoldsDeduplicatedMembers(): void $this->assertCount(count($unionType), $otherUnionType); $this->assertEqualsCanonicalizing( - $unionType->getIterator()->getArrayCopy(), - $otherUnionType->getIterator()->getArrayCopy() + $unionType->toArray(), + $otherUnionType->toArray() ); } From 88c6fd631ec967b722d961b5b012a19b408b2a31 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Wed, 26 Apr 2023 07:57:32 +0200 Subject: [PATCH 19/28] TASK: Rename `Inferrer` to `Narrower` and apply further suggestions from code review --- .../ExpressionTypeNarrower.php} | 28 +++++++++---------- .../NarrowedTypes.php} | 24 +++------------- .../TypeNarrowerContext.php} | 6 ++-- .../TernaryBranchScope/TernaryBranchScope.php | 12 ++++---- 4 files changed, 27 insertions(+), 43 deletions(-) rename src/TypeSystem/{Inferrer/TypeInferrer.php => Narrower/ExpressionTypeNarrower.php} (77%) rename src/TypeSystem/{Inferrer/InferredTypes.php => Narrower/NarrowedTypes.php} (69%) rename src/TypeSystem/{Inferrer/TypeInferrerContext.php => Narrower/TypeNarrowerContext.php} (90%) diff --git a/src/TypeSystem/Inferrer/TypeInferrer.php b/src/TypeSystem/Narrower/ExpressionTypeNarrower.php similarity index 77% rename from src/TypeSystem/Inferrer/TypeInferrer.php rename to src/TypeSystem/Narrower/ExpressionTypeNarrower.php index a4bcf1ac..9349f77b 100644 --- a/src/TypeSystem/Inferrer/TypeInferrer.php +++ b/src/TypeSystem/Narrower/ExpressionTypeNarrower.php @@ -20,7 +20,7 @@ declare(strict_types=1); -namespace PackageFactory\ComponentEngine\TypeSystem\Inferrer; +namespace PackageFactory\ComponentEngine\TypeSystem\Narrower; use PackageFactory\ComponentEngine\Definition\BinaryOperator; use PackageFactory\ComponentEngine\Parser\Ast\BinaryOperationNode; @@ -40,30 +40,30 @@ * The structure is partially inspired by phpstan * https://github.com/phpstan/phpstan-src/blob/07bb4aa2d5e39dafa78f56c5df132c763c2d1b67/src/Analyser/TypeSpecifier.php#L111 */ -class TypeInferrer +class ExpressionTypeNarrower { public function __construct( private readonly ScopeInterface $scope ) { } - public function inferTypesInCondition(ExpressionNode $conditionNode, TypeInferrerContext $context): InferredTypes + public function narrowTypesOfSymbolsIn(ExpressionNode $expressionNode, TypeNarrowerContext $context): NarrowedTypes { - if ($conditionNode->root instanceof IdentifierNode) { - $type = $this->scope->lookupTypeFor($conditionNode->root->value); + if ($expressionNode->root instanceof IdentifierNode) { + $type = $this->scope->lookupTypeFor($expressionNode->root->value); if (!$type) { - return InferredTypes::empty(); + return NarrowedTypes::empty(); } // case `nullableString ? "nullableString is not null" : "nullableString is null"` - return InferredTypes::fromType($conditionNode->root->value, $context->narrowDownType($type)); + return NarrowedTypes::fromEntry($expressionNode->root->value, $context->narrowType($type)); } - if (($binaryOperationNode = $conditionNode->root) instanceof BinaryOperationNode) { + if (($binaryOperationNode = $expressionNode->root) instanceof BinaryOperationNode) { // cases // `nullableString === null ? "nullableString is null" : "nullableString is not null"` // `nullableString !== null ? "nullableString is not null" : "nullableString is null"` if (count($binaryOperationNode->operands->rest) !== 1) { - return InferredTypes::empty(); + return NarrowedTypes::empty(); } $first = $binaryOperationNode->operands->first; $second = $binaryOperationNode->operands->rest[0]; @@ -77,21 +77,21 @@ public function inferTypesInCondition(ExpressionNode $conditionNode, TypeInferre }; if ($comparedIdentifierValueToNull === null) { - return InferredTypes::empty(); + return NarrowedTypes::empty(); } $type = $this->scope->lookupTypeFor($comparedIdentifierValueToNull); if (!$type) { - return InferredTypes::empty(); + return NarrowedTypes::empty(); } if ($binaryOperationNode->operator === BinaryOperator::EQUAL) { - return InferredTypes::fromType($comparedIdentifierValueToNull, $context->negate()->narrowDownType($type)); + return NarrowedTypes::fromEntry($comparedIdentifierValueToNull, $context->negate()->narrowType($type)); } if ($binaryOperationNode->operator === BinaryOperator::NOT_EQUAL) { - return InferredTypes::fromType($comparedIdentifierValueToNull, $context->narrowDownType($type)); + return NarrowedTypes::fromEntry($comparedIdentifierValueToNull, $context->narrowType($type)); } } - return InferredTypes::empty(); + return NarrowedTypes::empty(); } } diff --git a/src/TypeSystem/Inferrer/InferredTypes.php b/src/TypeSystem/Narrower/NarrowedTypes.php similarity index 69% rename from src/TypeSystem/Inferrer/InferredTypes.php rename to src/TypeSystem/Narrower/NarrowedTypes.php index bb106b9c..f8f1595f 100644 --- a/src/TypeSystem/Inferrer/InferredTypes.php +++ b/src/TypeSystem/Narrower/NarrowedTypes.php @@ -20,11 +20,11 @@ declare(strict_types=1); -namespace PackageFactory\ComponentEngine\TypeSystem\Inferrer; +namespace PackageFactory\ComponentEngine\TypeSystem\Narrower; use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; -class InferredTypes +class NarrowedTypes { /** * Map of identifierName to the corresponding inferred type @@ -35,7 +35,7 @@ class InferredTypes private function __construct( TypeInterface ...$types ) { - assert(self::isAssociativeArray($types), '$types must be an associative array'); + /** @var array $types */ $this->types = $types; } @@ -44,7 +44,7 @@ public static function empty(): self return new self(); } - public static function fromType(string $identifierName, TypeInterface $type): self + public static function fromEntry(string $identifierName, TypeInterface $type): self { return new self(...[$identifierName => $type]); } @@ -53,20 +53,4 @@ public function getType(string $identifierName): ?TypeInterface { return $this->types[$identifierName] ?? null; } - - /** - * @template T - * @param array $array - * @phpstan-assert-if-true array $array - */ - private static function isAssociativeArray(array $array): bool - { - foreach ($array as $key => $value) { - if (is_string($key)) { - continue; - } - return false; - } - return true; - } } diff --git a/src/TypeSystem/Inferrer/TypeInferrerContext.php b/src/TypeSystem/Narrower/TypeNarrowerContext.php similarity index 90% rename from src/TypeSystem/Inferrer/TypeInferrerContext.php rename to src/TypeSystem/Narrower/TypeNarrowerContext.php index 6c2d0066..dd683498 100644 --- a/src/TypeSystem/Inferrer/TypeInferrerContext.php +++ b/src/TypeSystem/Narrower/TypeNarrowerContext.php @@ -20,13 +20,13 @@ declare(strict_types=1); -namespace PackageFactory\ComponentEngine\TypeSystem\Inferrer; +namespace PackageFactory\ComponentEngine\TypeSystem\Narrower; use PackageFactory\ComponentEngine\TypeSystem\Type\NullType\NullType; use PackageFactory\ComponentEngine\TypeSystem\Type\UnionType\UnionType; use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; -enum TypeInferrerContext +enum TypeNarrowerContext { case TRUTHY; @@ -40,7 +40,7 @@ public function negate(): self }; } - public function narrowDownType(TypeInterface $type): TypeInterface + public function narrowType(TypeInterface $type): TypeInterface { if (!$type instanceof UnionType || !$type->containsNull()) { return $type; diff --git a/src/TypeSystem/Scope/TernaryBranchScope/TernaryBranchScope.php b/src/TypeSystem/Scope/TernaryBranchScope/TernaryBranchScope.php index c3024920..4615e071 100644 --- a/src/TypeSystem/Scope/TernaryBranchScope/TernaryBranchScope.php +++ b/src/TypeSystem/Scope/TernaryBranchScope/TernaryBranchScope.php @@ -24,16 +24,16 @@ use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNode; use PackageFactory\ComponentEngine\Parser\Ast\TypeReferenceNode; -use PackageFactory\ComponentEngine\TypeSystem\Inferrer\InferredTypes; -use PackageFactory\ComponentEngine\TypeSystem\Inferrer\TypeInferrer; -use PackageFactory\ComponentEngine\TypeSystem\Inferrer\TypeInferrerContext; +use PackageFactory\ComponentEngine\TypeSystem\Narrower\NarrowedTypes; +use PackageFactory\ComponentEngine\TypeSystem\Narrower\ExpressionTypeNarrower; +use PackageFactory\ComponentEngine\TypeSystem\Narrower\TypeNarrowerContext; use PackageFactory\ComponentEngine\TypeSystem\ScopeInterface; use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; final class TernaryBranchScope implements ScopeInterface { private function __construct( - private readonly InferredTypes $inferredTypes, + private readonly NarrowedTypes $inferredTypes, private readonly ScopeInterface $parentScope ) { } @@ -41,7 +41,7 @@ private function __construct( public static function forTruthyBranch(ExpressionNode $conditionNode, ScopeInterface $parentScope): self { return new self( - (new TypeInferrer($parentScope))->inferTypesInCondition($conditionNode, TypeInferrerContext::TRUTHY), + (new ExpressionTypeNarrower($parentScope))->narrowTypesOfSymbolsIn($conditionNode, TypeNarrowerContext::TRUTHY), $parentScope ); } @@ -49,7 +49,7 @@ public static function forTruthyBranch(ExpressionNode $conditionNode, ScopeInter public static function forFalsyBranch(ExpressionNode $conditionNode, ScopeInterface $parentScope): self { return new self( - (new TypeInferrer($parentScope))->inferTypesInCondition($conditionNode, TypeInferrerContext::FALSY), + (new ExpressionTypeNarrower($parentScope))->narrowTypesOfSymbolsIn($conditionNode, TypeNarrowerContext::FALSY), $parentScope ); } From e0a913a9577e1b98118b3582fe518bf80279fe8a Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Wed, 26 Apr 2023 08:27:08 +0200 Subject: [PATCH 20/28] TASK: `Narrower` handle boolean literal comparisons --- .../Narrower/ExpressionTypeNarrower.php | 35 +++++++++++++++++-- .../TernaryOperationTypeResolverTest.php | 25 ++++++++++--- 2 files changed, 53 insertions(+), 7 deletions(-) diff --git a/src/TypeSystem/Narrower/ExpressionTypeNarrower.php b/src/TypeSystem/Narrower/ExpressionTypeNarrower.php index 9349f77b..fb307bd4 100644 --- a/src/TypeSystem/Narrower/ExpressionTypeNarrower.php +++ b/src/TypeSystem/Narrower/ExpressionTypeNarrower.php @@ -24,6 +24,7 @@ use PackageFactory\ComponentEngine\Definition\BinaryOperator; use PackageFactory\ComponentEngine\Parser\Ast\BinaryOperationNode; +use PackageFactory\ComponentEngine\Parser\Ast\BooleanLiteralNode; use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNode; use PackageFactory\ComponentEngine\Parser\Ast\IdentifierNode; use PackageFactory\ComponentEngine\Parser\Ast\NullLiteralNode; @@ -59,15 +60,43 @@ public function narrowTypesOfSymbolsIn(ExpressionNode $expressionNode, TypeNarro } if (($binaryOperationNode = $expressionNode->root) instanceof BinaryOperationNode) { - // cases - // `nullableString === null ? "nullableString is null" : "nullableString is not null"` - // `nullableString !== null ? "nullableString is not null" : "nullableString is null"` + // todo we currently only work with two operands if (count($binaryOperationNode->operands->rest) !== 1) { return NarrowedTypes::empty(); } $first = $binaryOperationNode->operands->first; $second = $binaryOperationNode->operands->rest[0]; + if ( + ($first->root instanceof BooleanLiteralNode + && ($boolean = $first->root) instanceof BooleanLiteralNode + // @phpstan-ignore-next-line + && $other = $second + ) || ($second->root instanceof BooleanLiteralNode + && ($boolean = $second->root) instanceof BooleanLiteralNode + // @phpstan-ignore-next-line + && $other = $first + ) + ) { + $contextBasedOnOperator = match ($binaryOperationNode->operator) { + BinaryOperator::EQUAL => $context, + BinaryOperator::NOT_EQUAL => $context->negate(), + default => null, + }; + + if (!$contextBasedOnOperator) { + return NarrowedTypes::empty(); + } + + return $this->narrowTypesOfSymbolsIn( + $other, + $boolean->value ? $contextBasedOnOperator : $contextBasedOnOperator->negate() + ); + } + + // cases + // `nullableString === null ? "nullableString is null" : "nullableString is not null"` + // `nullableString !== null ? "nullableString is not null" : "nullableString is null"` $comparedIdentifierValueToNull = match (true) { // case `nullableString === null` $first->root instanceof IdentifierNode && $second->root instanceof NullLiteralNode => $first->root->value, diff --git a/test/Unit/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolverTest.php b/test/Unit/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolverTest.php index 0dc64d80..2adcbb98 100644 --- a/test/Unit/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolverTest.php +++ b/test/Unit/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolverTest.php @@ -45,27 +45,44 @@ public function ternaryOperationExamples(): array 'false ? 42 : "foo"' => ['false ? 42 : "foo"', StringType::get()], '1 < 2 ? 42 : "foo"' => ['1 < 2 ? 42 : "foo"', UnionType::of(NumberType::get(), StringType::get())], '1 < 2 ? variableOfTypeString : variableOfTypeNumber' => [ - '1 < 2 ? variableOfTypeString : variableOfTypeNumber', + '1 < 2 ? variableOfTypeString : variableOfTypeNumber', UnionType::of(NumberType::get(), StringType::get()) ], + 'nullableString ? nullableString : "fallback"' => [ 'nullableString ? nullableString : "fallback"', StringType::get() ], + 'nullableString ? null : nullableString' => [ + 'nullableString ? null : nullableString', NullType::get() + ], + 'nullableString === null ? "" : nullableString' => [ 'nullableString === null ? "" : nullableString', StringType::get() ], + // Patience you must have my young Padawan. 'null === nullableString ? "" : nullableString' => [ 'null === nullableString ? "" : nullableString', StringType::get() ], + 'nullableString !== null ? nullableString : ""' => [ 'nullableString !== null ? nullableString : ""', StringType::get() ], - // Patience you must have my young Padawan. 'null !== nullableString ? nullableString : ""' => [ 'null !== nullableString ? nullableString : ""', StringType::get() ], - 'nullableString ? null : nullableString' => [ - 'nullableString ? null : nullableString', NullType::get() + + 'true === (nullableString === null) ? "" : nullableString' => [ + 'true === (nullableString === null) ? "" : nullableString', StringType::get() + ], + 'false !== (nullableString === null) ? "" : nullableString' => [ + 'false !== (nullableString === null) ? "" : nullableString', StringType::get() + ], + + 'false === (nullableString === null) ? nullableString : ""' => [ + 'false === (nullableString === null) ? nullableString : ""', StringType::get() + ], + 'true !== (nullableString === null) ? nullableString : ""' => [ + 'true !== (nullableString === null) ? nullableString : ""', StringType::get() ], ]; } From aaf6c49882f30c057193fa3c64772c76737e40ba Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Wed, 26 Apr 2023 08:50:57 +0200 Subject: [PATCH 21/28] TASK: `Narrower` null comparison against any expression that resolves to null --- .../Narrower/ExpressionTypeNarrower.php | 68 ++++++++----------- .../Narrower/TypeNarrowerContext.php | 10 +++ .../TernaryOperationTypeResolverTest.php | 5 ++ 3 files changed, 44 insertions(+), 39 deletions(-) diff --git a/src/TypeSystem/Narrower/ExpressionTypeNarrower.php b/src/TypeSystem/Narrower/ExpressionTypeNarrower.php index fb307bd4..9b8fdd2b 100644 --- a/src/TypeSystem/Narrower/ExpressionTypeNarrower.php +++ b/src/TypeSystem/Narrower/ExpressionTypeNarrower.php @@ -22,13 +22,13 @@ namespace PackageFactory\ComponentEngine\TypeSystem\Narrower; -use PackageFactory\ComponentEngine\Definition\BinaryOperator; use PackageFactory\ComponentEngine\Parser\Ast\BinaryOperationNode; use PackageFactory\ComponentEngine\Parser\Ast\BooleanLiteralNode; use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNode; use PackageFactory\ComponentEngine\Parser\Ast\IdentifierNode; -use PackageFactory\ComponentEngine\Parser\Ast\NullLiteralNode; +use PackageFactory\ComponentEngine\TypeSystem\Resolver\Expression\ExpressionTypeResolver; use PackageFactory\ComponentEngine\TypeSystem\ScopeInterface; +use PackageFactory\ComponentEngine\TypeSystem\Type\NullType\NullType; /** * This class handles the analysis of identifier types that are used in a condition @@ -68,23 +68,13 @@ public function narrowTypesOfSymbolsIn(ExpressionNode $expressionNode, TypeNarro $second = $binaryOperationNode->operands->rest[0]; if ( - ($first->root instanceof BooleanLiteralNode - && ($boolean = $first->root) instanceof BooleanLiteralNode - // @phpstan-ignore-next-line - && $other = $second - ) || ($second->root instanceof BooleanLiteralNode - && ($boolean = $second->root) instanceof BooleanLiteralNode - // @phpstan-ignore-next-line - && $other = $first + (($boolean = $first->root) instanceof BooleanLiteralNode + && $other = $second // @phpstan-ignore-line + ) || (($boolean = $second->root) instanceof BooleanLiteralNode + && $other = $first // @phpstan-ignore-line ) ) { - $contextBasedOnOperator = match ($binaryOperationNode->operator) { - BinaryOperator::EQUAL => $context, - BinaryOperator::NOT_EQUAL => $context->negate(), - default => null, - }; - - if (!$contextBasedOnOperator) { + if (!$contextBasedOnOperator = $context->basedOnBinaryOperator($binaryOperationNode->operator)) { return NarrowedTypes::empty(); } @@ -94,30 +84,30 @@ public function narrowTypesOfSymbolsIn(ExpressionNode $expressionNode, TypeNarro ); } - // cases - // `nullableString === null ? "nullableString is null" : "nullableString is not null"` - // `nullableString !== null ? "nullableString is not null" : "nullableString is null"` - $comparedIdentifierValueToNull = match (true) { - // case `nullableString === null` - $first->root instanceof IdentifierNode && $second->root instanceof NullLiteralNode => $first->root->value, - // yodas case `null === nullableString` - $first->root instanceof NullLiteralNode && $second->root instanceof IdentifierNode => $second->root->value, - default => null - }; + $expressionTypeResolver = (new ExpressionTypeResolver($this->scope)); + if ( + ($expressionTypeResolver->resolveTypeOf($first)->is(NullType::get()) + && $other = $second // @phpstan-ignore-line + ) || ($expressionTypeResolver->resolveTypeOf($second)->is(NullType::get()) + && $other = $first // @phpstan-ignore-line + ) + ) { + if (!$other->root instanceof IdentifierNode) { + return NarrowedTypes::empty(); + } + $type = $this->scope->lookupTypeFor($other->root->value); + if (!$type) { + return NarrowedTypes::empty(); + } - if ($comparedIdentifierValueToNull === null) { - return NarrowedTypes::empty(); - } - $type = $this->scope->lookupTypeFor($comparedIdentifierValueToNull); - if (!$type) { - return NarrowedTypes::empty(); - } + if (!$contextBasedOnOperator = $context->basedOnBinaryOperator($binaryOperationNode->operator)) { + return NarrowedTypes::empty(); + } - if ($binaryOperationNode->operator === BinaryOperator::EQUAL) { - return NarrowedTypes::fromEntry($comparedIdentifierValueToNull, $context->negate()->narrowType($type)); - } - if ($binaryOperationNode->operator === BinaryOperator::NOT_EQUAL) { - return NarrowedTypes::fromEntry($comparedIdentifierValueToNull, $context->narrowType($type)); + return NarrowedTypes::fromEntry( + $other->root->value, + $contextBasedOnOperator->negate()->narrowType($type) + ); } } diff --git a/src/TypeSystem/Narrower/TypeNarrowerContext.php b/src/TypeSystem/Narrower/TypeNarrowerContext.php index dd683498..b556826c 100644 --- a/src/TypeSystem/Narrower/TypeNarrowerContext.php +++ b/src/TypeSystem/Narrower/TypeNarrowerContext.php @@ -22,6 +22,7 @@ namespace PackageFactory\ComponentEngine\TypeSystem\Narrower; +use PackageFactory\ComponentEngine\Definition\BinaryOperator; use PackageFactory\ComponentEngine\TypeSystem\Type\NullType\NullType; use PackageFactory\ComponentEngine\TypeSystem\Type\UnionType\UnionType; use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; @@ -40,6 +41,15 @@ public function negate(): self }; } + public function basedOnBinaryOperator(BinaryOperator $operator): ?self + { + return match ($operator) { + BinaryOperator::EQUAL => $this, + BinaryOperator::NOT_EQUAL => $this->negate(), + default => null, + }; + } + public function narrowType(TypeInterface $type): TypeInterface { if (!$type instanceof UnionType || !$type->containsNull()) { diff --git a/test/Unit/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolverTest.php b/test/Unit/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolverTest.php index 2adcbb98..e9b30c4e 100644 --- a/test/Unit/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolverTest.php +++ b/test/Unit/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolverTest.php @@ -84,6 +84,10 @@ public function ternaryOperationExamples(): array 'true !== (nullableString === null) ? nullableString : ""' => [ 'true !== (nullableString === null) ? nullableString : ""', StringType::get() ], + + 'nullableString === variableOfTypeNull ? "" : nullableString' => [ + 'nullableString === variableOfTypeNull ? "" : nullableString', StringType::get() + ], ]; } @@ -99,6 +103,7 @@ public function resolvesTernaryOperationToResultingType(string $ternaryExpressio $scope = new DummyScope([ 'variableOfTypeString' => StringType::get(), 'variableOfTypeNumber' => NumberType::get(), + 'variableOfTypeNull' => NullType::get(), 'nullableString' => UnionType::of(StringType::get(), NullType::get()) ]); $ternaryOperationTypeResolver = new TernaryOperationTypeResolver( From 560c97ddcca07c778c6726b9dd09415c5763789b Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Wed, 26 Apr 2023 09:15:09 +0200 Subject: [PATCH 22/28] TASK: Add `ExpressionTypeNarrowerTest` --- src/TypeSystem/Narrower/NarrowedTypes.php | 6 + .../Narrower/ExpressionTypeNarrowerTest.php | 104 ++++++++++++++++++ .../TernaryOperationTypeResolverTest.php | 26 ----- 3 files changed, 110 insertions(+), 26 deletions(-) create mode 100644 test/Unit/TypeSystem/Narrower/ExpressionTypeNarrowerTest.php diff --git a/src/TypeSystem/Narrower/NarrowedTypes.php b/src/TypeSystem/Narrower/NarrowedTypes.php index f8f1595f..8e2ff64c 100644 --- a/src/TypeSystem/Narrower/NarrowedTypes.php +++ b/src/TypeSystem/Narrower/NarrowedTypes.php @@ -53,4 +53,10 @@ public function getType(string $identifierName): ?TypeInterface { return $this->types[$identifierName] ?? null; } + + /** @return array */ + public function toArray(): array + { + return $this->types; + } } diff --git a/test/Unit/TypeSystem/Narrower/ExpressionTypeNarrowerTest.php b/test/Unit/TypeSystem/Narrower/ExpressionTypeNarrowerTest.php new file mode 100644 index 00000000..ef186abb --- /dev/null +++ b/test/Unit/TypeSystem/Narrower/ExpressionTypeNarrowerTest.php @@ -0,0 +1,104 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Narrower; + + +use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNode; +use PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Scope\Fixtures\DummyScope; +use PackageFactory\ComponentEngine\TypeSystem\Narrower\ExpressionTypeNarrower; +use PackageFactory\ComponentEngine\TypeSystem\Narrower\NarrowedTypes; +use PackageFactory\ComponentEngine\TypeSystem\Narrower\TypeNarrowerContext; +use PackageFactory\ComponentEngine\TypeSystem\Type\NullType\NullType; +use PackageFactory\ComponentEngine\TypeSystem\Type\StringType\StringType; +use PackageFactory\ComponentEngine\TypeSystem\Type\UnionType\UnionType; +use PHPUnit\Framework\TestCase; + +final class ExpressionTypeNarrowerTest extends TestCase +{ + public function narrowedExpressionsExamples(): mixed + { + return [ + 'nullableString' => [ + 'nullableString', + $variableIsString = NarrowedTypes::fromEntry('nullableString', StringType::get()) + ], + + 'nullableString === null' => [ + 'nullableString === null', + $variableIsNull = NarrowedTypes::fromEntry('nullableString', NullType::get()) + ], + // Patience you must have my young Padawan. + 'null === nullableString' => [ + 'null === nullableString', $variableIsNull + ], + + 'nullableString !== null' => [ + 'nullableString !== null', $variableIsString + ], + 'null !== nullableString' => [ + 'null !== nullableString', $variableIsString + ], + + 'true === (nullableString === null)' => [ + 'true === (nullableString === null)', $variableIsNull + ], + 'false !== (nullableString === null)' => [ + 'false !== (nullableString === null)', $variableIsNull + ], + + 'false === (nullableString === null)' => [ + 'false === (nullableString === null)', $variableIsString + ], + 'true !== (nullableString === null)' => [ + 'true !== (nullableString === null)', $variableIsString + ], + + 'nullableString === variableOfTypeNull' => [ + 'nullableString === variableOfTypeNull', $variableIsNull + ], + ]; + } + + /** + * @dataProvider narrowedExpressionsExamples + * @test + */ + public function narrowedExpressions(string $expressionAsString, NarrowedTypes $expectedTypes): void + { + $expressionTypeNarrower = new ExpressionTypeNarrower( + scope: new DummyScope([ + 'nullableString' => UnionType::of(StringType::get(), NullType::get()), + 'variableOfTypeNull' => NullType::get() + ]) + ); + + $expressionNode = ExpressionNode::fromString($expressionAsString); + + $actualTypes = $expressionTypeNarrower->narrowTypesOfSymbolsIn($expressionNode, TypeNarrowerContext::TRUTHY); + + $this->assertEqualsCanonicalizing( + $expectedTypes->toArray(), + $actualTypes->toArray() + ); + } +} diff --git a/test/Unit/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolverTest.php b/test/Unit/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolverTest.php index e9b30c4e..430cdb26 100644 --- a/test/Unit/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolverTest.php +++ b/test/Unit/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolverTest.php @@ -63,31 +63,6 @@ public function ternaryOperationExamples(): array 'null === nullableString ? "" : nullableString' => [ 'null === nullableString ? "" : nullableString', StringType::get() ], - - 'nullableString !== null ? nullableString : ""' => [ - 'nullableString !== null ? nullableString : ""', StringType::get() - ], - 'null !== nullableString ? nullableString : ""' => [ - 'null !== nullableString ? nullableString : ""', StringType::get() - ], - - 'true === (nullableString === null) ? "" : nullableString' => [ - 'true === (nullableString === null) ? "" : nullableString', StringType::get() - ], - 'false !== (nullableString === null) ? "" : nullableString' => [ - 'false !== (nullableString === null) ? "" : nullableString', StringType::get() - ], - - 'false === (nullableString === null) ? nullableString : ""' => [ - 'false === (nullableString === null) ? nullableString : ""', StringType::get() - ], - 'true !== (nullableString === null) ? nullableString : ""' => [ - 'true !== (nullableString === null) ? nullableString : ""', StringType::get() - ], - - 'nullableString === variableOfTypeNull ? "" : nullableString' => [ - 'nullableString === variableOfTypeNull ? "" : nullableString', StringType::get() - ], ]; } @@ -103,7 +78,6 @@ public function resolvesTernaryOperationToResultingType(string $ternaryExpressio $scope = new DummyScope([ 'variableOfTypeString' => StringType::get(), 'variableOfTypeNumber' => NumberType::get(), - 'variableOfTypeNull' => NullType::get(), 'nullableString' => UnionType::of(StringType::get(), NullType::get()) ]); $ternaryOperationTypeResolver = new TernaryOperationTypeResolver( From d00a1942c77d2e398151655a2a0cfcc23b4e7357 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Wed, 26 Apr 2023 09:48:59 +0200 Subject: [PATCH 23/28] TASK: Don't narrow `nullableString === true` as string --- src/TypeSystem/Narrower/ExpressionTypeNarrower.php | 4 ++++ test/Unit/TypeSystem/Narrower/ExpressionTypeNarrowerTest.php | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/src/TypeSystem/Narrower/ExpressionTypeNarrower.php b/src/TypeSystem/Narrower/ExpressionTypeNarrower.php index 9b8fdd2b..5189429f 100644 --- a/src/TypeSystem/Narrower/ExpressionTypeNarrower.php +++ b/src/TypeSystem/Narrower/ExpressionTypeNarrower.php @@ -78,6 +78,10 @@ public function narrowTypesOfSymbolsIn(ExpressionNode $expressionNode, TypeNarro return NarrowedTypes::empty(); } + if ($other->root instanceof IdentifierNode) { + return NarrowedTypes::empty(); + } + return $this->narrowTypesOfSymbolsIn( $other, $boolean->value ? $contextBasedOnOperator : $contextBasedOnOperator->negate() diff --git a/test/Unit/TypeSystem/Narrower/ExpressionTypeNarrowerTest.php b/test/Unit/TypeSystem/Narrower/ExpressionTypeNarrowerTest.php index ef186abb..c4ba9d4f 100644 --- a/test/Unit/TypeSystem/Narrower/ExpressionTypeNarrowerTest.php +++ b/test/Unit/TypeSystem/Narrower/ExpressionTypeNarrowerTest.php @@ -76,6 +76,11 @@ public function narrowedExpressionsExamples(): mixed 'nullableString === variableOfTypeNull' => [ 'nullableString === variableOfTypeNull', $variableIsNull ], + + 'nullableString === true' => [ + 'nullableString === true', + NarrowedTypes::empty() + ], ]; } From 530b155b80f2eddf1cd6514f7ccff4151ba360bc Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Wed, 26 Apr 2023 10:03:46 +0200 Subject: [PATCH 24/28] TASK: Narrow `nullableString && true` --- .../Narrower/ExpressionTypeNarrower.php | 29 +++++++++++++------ .../Narrower/ExpressionTypeNarrowerTest.php | 20 +++++++++++-- 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/src/TypeSystem/Narrower/ExpressionTypeNarrower.php b/src/TypeSystem/Narrower/ExpressionTypeNarrower.php index 5189429f..26594586 100644 --- a/src/TypeSystem/Narrower/ExpressionTypeNarrower.php +++ b/src/TypeSystem/Narrower/ExpressionTypeNarrower.php @@ -22,6 +22,7 @@ namespace PackageFactory\ComponentEngine\TypeSystem\Narrower; +use PackageFactory\ComponentEngine\Definition\BinaryOperator; use PackageFactory\ComponentEngine\Parser\Ast\BinaryOperationNode; use PackageFactory\ComponentEngine\Parser\Ast\BooleanLiteralNode; use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNode; @@ -74,18 +75,28 @@ public function narrowTypesOfSymbolsIn(ExpressionNode $expressionNode, TypeNarro && $other = $first // @phpstan-ignore-line ) ) { - if (!$contextBasedOnOperator = $context->basedOnBinaryOperator($binaryOperationNode->operator)) { - return NarrowedTypes::empty(); - } + switch ($binaryOperationNode->operator) { + case BinaryOperator::AND: + if ($boolean->value && $context === TypeNarrowerContext::TRUTHY) { + return $this->narrowTypesOfSymbolsIn($other, $context); + } + break; + case BinaryOperator::EQUAL: + case BinaryOperator::NOT_EQUAL: + $contextBasedOnOperator = $context->basedOnBinaryOperator($binaryOperationNode->operator); + assert($contextBasedOnOperator !== null); - if ($other->root instanceof IdentifierNode) { - return NarrowedTypes::empty(); + if ($other->root instanceof IdentifierNode) { + return NarrowedTypes::empty(); + } + + return $this->narrowTypesOfSymbolsIn( + $other, + $boolean->value ? $contextBasedOnOperator : $contextBasedOnOperator->negate() + ); } - return $this->narrowTypesOfSymbolsIn( - $other, - $boolean->value ? $contextBasedOnOperator : $contextBasedOnOperator->negate() - ); + return NarrowedTypes::empty(); } $expressionTypeResolver = (new ExpressionTypeResolver($this->scope)); diff --git a/test/Unit/TypeSystem/Narrower/ExpressionTypeNarrowerTest.php b/test/Unit/TypeSystem/Narrower/ExpressionTypeNarrowerTest.php index c4ba9d4f..94a0f7ba 100644 --- a/test/Unit/TypeSystem/Narrower/ExpressionTypeNarrowerTest.php +++ b/test/Unit/TypeSystem/Narrower/ExpressionTypeNarrowerTest.php @@ -77,6 +77,17 @@ public function narrowedExpressionsExamples(): mixed 'nullableString === variableOfTypeNull', $variableIsNull ], + 'nullableString && true' => [ + 'nullableString && true', + $variableIsString + ], + + 'FALSY:: nullableString && true' => [ + 'nullableString && true', + NarrowedTypes::empty(), + TypeNarrowerContext::FALSY + ], + 'nullableString === true' => [ 'nullableString === true', NarrowedTypes::empty() @@ -88,8 +99,11 @@ public function narrowedExpressionsExamples(): mixed * @dataProvider narrowedExpressionsExamples * @test */ - public function narrowedExpressions(string $expressionAsString, NarrowedTypes $expectedTypes): void - { + public function narrowedExpressions( + string $expressionAsString, + NarrowedTypes $expectedTypes, + TypeNarrowerContext $context = TypeNarrowerContext::TRUTHY + ): void { $expressionTypeNarrower = new ExpressionTypeNarrower( scope: new DummyScope([ 'nullableString' => UnionType::of(StringType::get(), NullType::get()), @@ -99,7 +113,7 @@ public function narrowedExpressions(string $expressionAsString, NarrowedTypes $e $expressionNode = ExpressionNode::fromString($expressionAsString); - $actualTypes = $expressionTypeNarrower->narrowTypesOfSymbolsIn($expressionNode, TypeNarrowerContext::TRUTHY); + $actualTypes = $expressionTypeNarrower->narrowTypesOfSymbolsIn($expressionNode, $context); $this->assertEqualsCanonicalizing( $expectedTypes->toArray(), From 331cddaaf51a4270f19f2c5f5dbec9ec819971e9 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Wed, 26 Apr 2023 10:06:29 +0200 Subject: [PATCH 25/28] TASK: Correct namespace --- .../Narrower/{ => Expression}/ExpressionTypeNarrower.php | 4 +++- .../Scope/TernaryBranchScope/TernaryBranchScope.php | 6 +++--- .../{ => Expression}/ExpressionTypeNarrowerTest.php | 4 ++-- 3 files changed, 8 insertions(+), 6 deletions(-) rename src/TypeSystem/Narrower/{ => Expression}/ExpressionTypeNarrower.php (96%) rename test/Unit/TypeSystem/Narrower/{ => Expression}/ExpressionTypeNarrowerTest.php (99%) diff --git a/src/TypeSystem/Narrower/ExpressionTypeNarrower.php b/src/TypeSystem/Narrower/Expression/ExpressionTypeNarrower.php similarity index 96% rename from src/TypeSystem/Narrower/ExpressionTypeNarrower.php rename to src/TypeSystem/Narrower/Expression/ExpressionTypeNarrower.php index 26594586..ab65da98 100644 --- a/src/TypeSystem/Narrower/ExpressionTypeNarrower.php +++ b/src/TypeSystem/Narrower/Expression/ExpressionTypeNarrower.php @@ -20,13 +20,15 @@ declare(strict_types=1); -namespace PackageFactory\ComponentEngine\TypeSystem\Narrower; +namespace PackageFactory\ComponentEngine\TypeSystem\Narrower\Expression; use PackageFactory\ComponentEngine\Definition\BinaryOperator; use PackageFactory\ComponentEngine\Parser\Ast\BinaryOperationNode; use PackageFactory\ComponentEngine\Parser\Ast\BooleanLiteralNode; use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNode; use PackageFactory\ComponentEngine\Parser\Ast\IdentifierNode; +use PackageFactory\ComponentEngine\TypeSystem\Narrower\NarrowedTypes; +use PackageFactory\ComponentEngine\TypeSystem\Narrower\TypeNarrowerContext; use PackageFactory\ComponentEngine\TypeSystem\Resolver\Expression\ExpressionTypeResolver; use PackageFactory\ComponentEngine\TypeSystem\ScopeInterface; use PackageFactory\ComponentEngine\TypeSystem\Type\NullType\NullType; diff --git a/src/TypeSystem/Scope/TernaryBranchScope/TernaryBranchScope.php b/src/TypeSystem/Scope/TernaryBranchScope/TernaryBranchScope.php index 4615e071..4d6951ab 100644 --- a/src/TypeSystem/Scope/TernaryBranchScope/TernaryBranchScope.php +++ b/src/TypeSystem/Scope/TernaryBranchScope/TernaryBranchScope.php @@ -25,7 +25,7 @@ use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNode; use PackageFactory\ComponentEngine\Parser\Ast\TypeReferenceNode; use PackageFactory\ComponentEngine\TypeSystem\Narrower\NarrowedTypes; -use PackageFactory\ComponentEngine\TypeSystem\Narrower\ExpressionTypeNarrower; +use PackageFactory\ComponentEngine\TypeSystem\Narrower\Expression\ExpressionTypeNarrower; use PackageFactory\ComponentEngine\TypeSystem\Narrower\TypeNarrowerContext; use PackageFactory\ComponentEngine\TypeSystem\ScopeInterface; use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; @@ -33,7 +33,7 @@ final class TernaryBranchScope implements ScopeInterface { private function __construct( - private readonly NarrowedTypes $inferredTypes, + private readonly NarrowedTypes $narrowedTypes, private readonly ScopeInterface $parentScope ) { } @@ -56,7 +56,7 @@ public static function forFalsyBranch(ExpressionNode $conditionNode, ScopeInterf public function lookupTypeFor(string $name): ?TypeInterface { - return $this->inferredTypes->getType($name) ?? $this->parentScope->lookupTypeFor($name); + return $this->narrowedTypes->getType($name) ?? $this->parentScope->lookupTypeFor($name); } public function resolveTypeReference(TypeReferenceNode $typeReferenceNode): TypeInterface diff --git a/test/Unit/TypeSystem/Narrower/ExpressionTypeNarrowerTest.php b/test/Unit/TypeSystem/Narrower/Expression/ExpressionTypeNarrowerTest.php similarity index 99% rename from test/Unit/TypeSystem/Narrower/ExpressionTypeNarrowerTest.php rename to test/Unit/TypeSystem/Narrower/Expression/ExpressionTypeNarrowerTest.php index 94a0f7ba..bc02555b 100644 --- a/test/Unit/TypeSystem/Narrower/ExpressionTypeNarrowerTest.php +++ b/test/Unit/TypeSystem/Narrower/Expression/ExpressionTypeNarrowerTest.php @@ -20,12 +20,12 @@ declare(strict_types=1); -namespace PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Narrower; +namespace PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Narrower\Expression; use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNode; use PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Scope\Fixtures\DummyScope; -use PackageFactory\ComponentEngine\TypeSystem\Narrower\ExpressionTypeNarrower; +use PackageFactory\ComponentEngine\TypeSystem\Narrower\Expression\ExpressionTypeNarrower; use PackageFactory\ComponentEngine\TypeSystem\Narrower\NarrowedTypes; use PackageFactory\ComponentEngine\TypeSystem\Narrower\TypeNarrowerContext; use PackageFactory\ComponentEngine\TypeSystem\Type\NullType\NullType; From 1027e16ce2d5087e62126b74e114fc26727310cb Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 29 Apr 2023 18:24:47 +0200 Subject: [PATCH 26/28] TASK: Adjust to BinaryOperationNode api change --- .../Expression/ExpressionTypeNarrower.php | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/TypeSystem/Narrower/Expression/ExpressionTypeNarrower.php b/src/TypeSystem/Narrower/Expression/ExpressionTypeNarrower.php index ab65da98..ceefafe0 100644 --- a/src/TypeSystem/Narrower/Expression/ExpressionTypeNarrower.php +++ b/src/TypeSystem/Narrower/Expression/ExpressionTypeNarrower.php @@ -63,18 +63,14 @@ public function narrowTypesOfSymbolsIn(ExpressionNode $expressionNode, TypeNarro } if (($binaryOperationNode = $expressionNode->root) instanceof BinaryOperationNode) { - // todo we currently only work with two operands - if (count($binaryOperationNode->operands->rest) !== 1) { - return NarrowedTypes::empty(); - } - $first = $binaryOperationNode->operands->first; - $second = $binaryOperationNode->operands->rest[0]; + $right = $binaryOperationNode->right; + $left = $binaryOperationNode->left; if ( - (($boolean = $first->root) instanceof BooleanLiteralNode - && $other = $second // @phpstan-ignore-line - ) || (($boolean = $second->root) instanceof BooleanLiteralNode - && $other = $first // @phpstan-ignore-line + (($boolean = $right->root) instanceof BooleanLiteralNode + && $other = $left // @phpstan-ignore-line + ) || (($boolean = $left->root) instanceof BooleanLiteralNode + && $other = $right // @phpstan-ignore-line ) ) { switch ($binaryOperationNode->operator) { @@ -103,10 +99,10 @@ public function narrowTypesOfSymbolsIn(ExpressionNode $expressionNode, TypeNarro $expressionTypeResolver = (new ExpressionTypeResolver($this->scope)); if ( - ($expressionTypeResolver->resolveTypeOf($first)->is(NullType::get()) - && $other = $second // @phpstan-ignore-line - ) || ($expressionTypeResolver->resolveTypeOf($second)->is(NullType::get()) - && $other = $first // @phpstan-ignore-line + ($expressionTypeResolver->resolveTypeOf($right)->is(NullType::get()) + && $other = $left // @phpstan-ignore-line + ) || ($expressionTypeResolver->resolveTypeOf($left)->is(NullType::get()) + && $other = $right // @phpstan-ignore-line ) ) { if (!$other->root instanceof IdentifierNode) { From 884b895e067f276bef49eecb7f494e9f96c95140 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 29 Apr 2023 18:55:41 +0200 Subject: [PATCH 27/28] TASK: Apply suggestions from code review remove Truthiness from api thanks @fcool for the discussion --- .../Expression/ExpressionTypeNarrower.php | 39 ++++++++++++------- ...TypeNarrowerContext.php => Truthiness.php} | 2 +- .../TernaryBranchScope/TernaryBranchScope.php | 7 ++-- .../Expression/ExpressionTypeNarrowerTest.php | 21 +++++----- 4 files changed, 41 insertions(+), 28 deletions(-) rename src/TypeSystem/Narrower/{TypeNarrowerContext.php => Truthiness.php} (98%) diff --git a/src/TypeSystem/Narrower/Expression/ExpressionTypeNarrower.php b/src/TypeSystem/Narrower/Expression/ExpressionTypeNarrower.php index ceefafe0..a6f0470e 100644 --- a/src/TypeSystem/Narrower/Expression/ExpressionTypeNarrower.php +++ b/src/TypeSystem/Narrower/Expression/ExpressionTypeNarrower.php @@ -28,7 +28,7 @@ use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNode; use PackageFactory\ComponentEngine\Parser\Ast\IdentifierNode; use PackageFactory\ComponentEngine\TypeSystem\Narrower\NarrowedTypes; -use PackageFactory\ComponentEngine\TypeSystem\Narrower\TypeNarrowerContext; +use PackageFactory\ComponentEngine\TypeSystem\Narrower\Truthiness; use PackageFactory\ComponentEngine\TypeSystem\Resolver\Expression\ExpressionTypeResolver; use PackageFactory\ComponentEngine\TypeSystem\ScopeInterface; use PackageFactory\ComponentEngine\TypeSystem\Type\NullType\NullType; @@ -38,20 +38,32 @@ * and based on the requested branch: truthy or falsy, will predict the types a variable will have in the respective branch * so it matches the expected runtime behaviour * - * For example given this expression: `nullableString ? "nullableString is not null" : "nullableString is null"` based on the condition `nullableString` - * It will infer that in the truthy context nullableString is a string while in the falsy context it will infer that it is a null + * For example given this expression: `nullableString ? nullableString : "fallback"` based on the condition `nullableString` + * it will infer that in the truthy context nullableString is a string while in the falsy context it will infer that it is null. + * In the above case the ternary expression will resolve to a string. * * The structure is partially inspired by phpstan * https://github.com/phpstan/phpstan-src/blob/07bb4aa2d5e39dafa78f56c5df132c763c2d1b67/src/Analyser/TypeSpecifier.php#L111 */ class ExpressionTypeNarrower { - public function __construct( - private readonly ScopeInterface $scope + private function __construct( + private readonly ScopeInterface $scope, + private readonly Truthiness $assumedTruthiness ) { } - public function narrowTypesOfSymbolsIn(ExpressionNode $expressionNode, TypeNarrowerContext $context): NarrowedTypes + public static function forTruthy(ScopeInterface $scope): self + { + return new self($scope, Truthiness::TRUTHY); + } + + public static function forFalsy(ScopeInterface $scope): self + { + return new self($scope, Truthiness::FALSY); + } + + public function narrowTypesOfSymbolsIn(ExpressionNode $expressionNode): NarrowedTypes { if ($expressionNode->root instanceof IdentifierNode) { $type = $this->scope->lookupTypeFor($expressionNode->root->value); @@ -59,7 +71,7 @@ public function narrowTypesOfSymbolsIn(ExpressionNode $expressionNode, TypeNarro return NarrowedTypes::empty(); } // case `nullableString ? "nullableString is not null" : "nullableString is null"` - return NarrowedTypes::fromEntry($expressionNode->root->value, $context->narrowType($type)); + return NarrowedTypes::fromEntry($expressionNode->root->value, $this->assumedTruthiness->narrowType($type)); } if (($binaryOperationNode = $expressionNode->root) instanceof BinaryOperationNode) { @@ -75,23 +87,24 @@ public function narrowTypesOfSymbolsIn(ExpressionNode $expressionNode, TypeNarro ) { switch ($binaryOperationNode->operator) { case BinaryOperator::AND: - if ($boolean->value && $context === TypeNarrowerContext::TRUTHY) { - return $this->narrowTypesOfSymbolsIn($other, $context); + if ($boolean->value && $this->assumedTruthiness === Truthiness::TRUTHY) { + return $this->narrowTypesOfSymbolsIn($other); } break; case BinaryOperator::EQUAL: case BinaryOperator::NOT_EQUAL: - $contextBasedOnOperator = $context->basedOnBinaryOperator($binaryOperationNode->operator); + $contextBasedOnOperator = $this->assumedTruthiness->basedOnBinaryOperator($binaryOperationNode->operator); assert($contextBasedOnOperator !== null); if ($other->root instanceof IdentifierNode) { return NarrowedTypes::empty(); } - return $this->narrowTypesOfSymbolsIn( - $other, + $subNarrower = new self( + $this->scope, $boolean->value ? $contextBasedOnOperator : $contextBasedOnOperator->negate() ); + return $subNarrower->narrowTypesOfSymbolsIn($other); } return NarrowedTypes::empty(); @@ -113,7 +126,7 @@ public function narrowTypesOfSymbolsIn(ExpressionNode $expressionNode, TypeNarro return NarrowedTypes::empty(); } - if (!$contextBasedOnOperator = $context->basedOnBinaryOperator($binaryOperationNode->operator)) { + if (!$contextBasedOnOperator = $this->assumedTruthiness->basedOnBinaryOperator($binaryOperationNode->operator)) { return NarrowedTypes::empty(); } diff --git a/src/TypeSystem/Narrower/TypeNarrowerContext.php b/src/TypeSystem/Narrower/Truthiness.php similarity index 98% rename from src/TypeSystem/Narrower/TypeNarrowerContext.php rename to src/TypeSystem/Narrower/Truthiness.php index b556826c..b839fea3 100644 --- a/src/TypeSystem/Narrower/TypeNarrowerContext.php +++ b/src/TypeSystem/Narrower/Truthiness.php @@ -27,7 +27,7 @@ use PackageFactory\ComponentEngine\TypeSystem\Type\UnionType\UnionType; use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; -enum TypeNarrowerContext +enum Truthiness { case TRUTHY; diff --git a/src/TypeSystem/Scope/TernaryBranchScope/TernaryBranchScope.php b/src/TypeSystem/Scope/TernaryBranchScope/TernaryBranchScope.php index 4d6951ab..c2092839 100644 --- a/src/TypeSystem/Scope/TernaryBranchScope/TernaryBranchScope.php +++ b/src/TypeSystem/Scope/TernaryBranchScope/TernaryBranchScope.php @@ -26,13 +26,12 @@ use PackageFactory\ComponentEngine\Parser\Ast\TypeReferenceNode; use PackageFactory\ComponentEngine\TypeSystem\Narrower\NarrowedTypes; use PackageFactory\ComponentEngine\TypeSystem\Narrower\Expression\ExpressionTypeNarrower; -use PackageFactory\ComponentEngine\TypeSystem\Narrower\TypeNarrowerContext; use PackageFactory\ComponentEngine\TypeSystem\ScopeInterface; use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; final class TernaryBranchScope implements ScopeInterface { - private function __construct( + public function __construct( private readonly NarrowedTypes $narrowedTypes, private readonly ScopeInterface $parentScope ) { @@ -41,7 +40,7 @@ private function __construct( public static function forTruthyBranch(ExpressionNode $conditionNode, ScopeInterface $parentScope): self { return new self( - (new ExpressionTypeNarrower($parentScope))->narrowTypesOfSymbolsIn($conditionNode, TypeNarrowerContext::TRUTHY), + ExpressionTypeNarrower::forTruthy($parentScope)->narrowTypesOfSymbolsIn($conditionNode), $parentScope ); } @@ -49,7 +48,7 @@ public static function forTruthyBranch(ExpressionNode $conditionNode, ScopeInter public static function forFalsyBranch(ExpressionNode $conditionNode, ScopeInterface $parentScope): self { return new self( - (new ExpressionTypeNarrower($parentScope))->narrowTypesOfSymbolsIn($conditionNode, TypeNarrowerContext::FALSY), + ExpressionTypeNarrower::forFalsy($parentScope)->narrowTypesOfSymbolsIn($conditionNode), $parentScope ); } diff --git a/test/Unit/TypeSystem/Narrower/Expression/ExpressionTypeNarrowerTest.php b/test/Unit/TypeSystem/Narrower/Expression/ExpressionTypeNarrowerTest.php index bc02555b..fb2c7905 100644 --- a/test/Unit/TypeSystem/Narrower/Expression/ExpressionTypeNarrowerTest.php +++ b/test/Unit/TypeSystem/Narrower/Expression/ExpressionTypeNarrowerTest.php @@ -27,7 +27,6 @@ use PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Scope\Fixtures\DummyScope; use PackageFactory\ComponentEngine\TypeSystem\Narrower\Expression\ExpressionTypeNarrower; use PackageFactory\ComponentEngine\TypeSystem\Narrower\NarrowedTypes; -use PackageFactory\ComponentEngine\TypeSystem\Narrower\TypeNarrowerContext; use PackageFactory\ComponentEngine\TypeSystem\Type\NullType\NullType; use PackageFactory\ComponentEngine\TypeSystem\Type\StringType\StringType; use PackageFactory\ComponentEngine\TypeSystem\Type\UnionType\UnionType; @@ -85,7 +84,7 @@ public function narrowedExpressionsExamples(): mixed 'FALSY:: nullableString && true' => [ 'nullableString && true', NarrowedTypes::empty(), - TypeNarrowerContext::FALSY + false ], 'nullableString === true' => [ @@ -102,18 +101,20 @@ public function narrowedExpressionsExamples(): mixed public function narrowedExpressions( string $expressionAsString, NarrowedTypes $expectedTypes, - TypeNarrowerContext $context = TypeNarrowerContext::TRUTHY + bool $truthiness = true ): void { - $expressionTypeNarrower = new ExpressionTypeNarrower( - scope: new DummyScope([ - 'nullableString' => UnionType::of(StringType::get(), NullType::get()), - 'variableOfTypeNull' => NullType::get() - ]) - ); + $scope = new DummyScope([ + 'nullableString' => UnionType::of(StringType::get(), NullType::get()), + 'variableOfTypeNull' => NullType::get() + ]); + + $expressionTypeNarrower = $truthiness + ? ExpressionTypeNarrower::forTruthy($scope) + : ExpressionTypeNarrower::forFalsy($scope); $expressionNode = ExpressionNode::fromString($expressionAsString); - $actualTypes = $expressionTypeNarrower->narrowTypesOfSymbolsIn($expressionNode, $context); + $actualTypes = $expressionTypeNarrower->narrowTypesOfSymbolsIn($expressionNode); $this->assertEqualsCanonicalizing( $expectedTypes->toArray(), From 012124773deee6f6fd4fd050ebb09d2a6b05c900 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 29 Apr 2023 19:04:30 +0200 Subject: [PATCH 28/28] TASK: ExpressionTypeNarrower support UnaryOperationNode --- .../Narrower/Expression/ExpressionTypeNarrower.php | 9 +++++++++ .../Narrower/Expression/ExpressionTypeNarrowerTest.php | 8 ++++++++ 2 files changed, 17 insertions(+) diff --git a/src/TypeSystem/Narrower/Expression/ExpressionTypeNarrower.php b/src/TypeSystem/Narrower/Expression/ExpressionTypeNarrower.php index a6f0470e..5cac539f 100644 --- a/src/TypeSystem/Narrower/Expression/ExpressionTypeNarrower.php +++ b/src/TypeSystem/Narrower/Expression/ExpressionTypeNarrower.php @@ -27,6 +27,7 @@ use PackageFactory\ComponentEngine\Parser\Ast\BooleanLiteralNode; use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNode; use PackageFactory\ComponentEngine\Parser\Ast\IdentifierNode; +use PackageFactory\ComponentEngine\Parser\Ast\UnaryOperationNode; use PackageFactory\ComponentEngine\TypeSystem\Narrower\NarrowedTypes; use PackageFactory\ComponentEngine\TypeSystem\Narrower\Truthiness; use PackageFactory\ComponentEngine\TypeSystem\Resolver\Expression\ExpressionTypeResolver; @@ -137,6 +138,14 @@ public function narrowTypesOfSymbolsIn(ExpressionNode $expressionNode): Narrowed } } + if (($unaryOperationNode = $expressionNode->root) instanceof UnaryOperationNode) { + $subNarrower = new self( + $this->scope, + $this->assumedTruthiness->negate() + ); + return $subNarrower->narrowTypesOfSymbolsIn($unaryOperationNode->argument); + } + return NarrowedTypes::empty(); } } diff --git a/test/Unit/TypeSystem/Narrower/Expression/ExpressionTypeNarrowerTest.php b/test/Unit/TypeSystem/Narrower/Expression/ExpressionTypeNarrowerTest.php index fb2c7905..060eefa7 100644 --- a/test/Unit/TypeSystem/Narrower/Expression/ExpressionTypeNarrowerTest.php +++ b/test/Unit/TypeSystem/Narrower/Expression/ExpressionTypeNarrowerTest.php @@ -91,6 +91,14 @@ public function narrowedExpressionsExamples(): mixed 'nullableString === true', NarrowedTypes::empty() ], + + '!nullableString' => [ + '!nullableString', $variableIsNull + ], + + '!!nullableString' => [ + '!!nullableString', $variableIsString + ], ]; }