Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Ternary Expression TypeNarrower for Nullables #14

Draft
wants to merge 30 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
7efcfd3
TASK: Put domain logic inside constructor of UnionType
mhsdesign Apr 9, 2023
2ffe099
TASK: Simple Nullable Handling
mhsdesign Apr 9, 2023
6293145
TASK: Infer types in arms of ternary
mhsdesign Apr 9, 2023
2a08474
TASK: Revert 7efcfd3cc179443912b5e06d1b913efde3cb5739
mhsdesign Apr 9, 2023
5e4d6ce
TASK: Union test that all members are deduplicated
mhsdesign Apr 9, 2023
5c08a4e
TASK: Union test isNullable and withoutNullable
mhsdesign Apr 9, 2023
8f0ba11
TASK: UnionType rename to containsNull, withoutNull
mhsdesign Apr 21, 2023
8a34ee1
TASK: Introduce TernaryBranchScope
mhsdesign Apr 21, 2023
1b476e5
TASK: Type inference for null comparison in ternary
mhsdesign Apr 21, 2023
57ee20c
TASK: UnionType RequiresAtLeastOneMember
mhsdesign Apr 21, 2023
3487bd9
TASK: TernaryBranchScope introduce static factories and dont throw bo…
mhsdesign Apr 21, 2023
1348ac1
TASK: Make type inference in TernaryBranchScope more explicit
mhsdesign Apr 22, 2023
492e05c
TASK: Adjust naming of $nonNullable to $typeWithoutNull
mhsdesign Apr 22, 2023
0c7061f
TASK: Solve #7 rudimentary
mhsdesign Apr 22, 2023
22df4ba
TASK: Introduce TypeInferrer inspired by phpstan to support future ad…
mhsdesign Apr 22, 2023
0adb4c0
TASK: Cleanup InferredTypes and extract duplicated logic to TypeInfer…
mhsdesign Apr 23, 2023
f61b896
TASK: Remove `@phpstan-ignore-next-line` by asserting that an array i…
mhsdesign Apr 23, 2023
6a8bd00
Merge remote-tracking branch 'origin/main' into task/simpleNullableHa…
mhsdesign Apr 23, 2023
d835fa5
TASK: UnionType::getIterator use `yield from`
mhsdesign Apr 26, 2023
88c6fd6
TASK: Rename `Inferrer` to `Narrower` and apply further suggestions f…
mhsdesign Apr 26, 2023
e0a913a
TASK: `Narrower` handle boolean literal comparisons
mhsdesign Apr 26, 2023
aaf6c49
TASK: `Narrower` null comparison against any expression that resolves…
mhsdesign Apr 26, 2023
560c97d
TASK: Add `ExpressionTypeNarrowerTest`
mhsdesign Apr 26, 2023
d00a194
TASK: Don't narrow `nullableString === true` as string
mhsdesign Apr 26, 2023
530b155
TASK: Narrow `nullableString && true`
mhsdesign Apr 26, 2023
331cdda
TASK: Correct namespace
mhsdesign Apr 26, 2023
bd28d87
Merge remote-tracking branch 'origin' into task/simpleNullableHandling
mhsdesign Apr 29, 2023
1027e16
TASK: Adjust to BinaryOperationNode api change
mhsdesign Apr 29, 2023
884b895
TASK: Apply suggestions from code review
mhsdesign Apr 29, 2023
0121247
TASK: ExpressionTypeNarrower support UnaryOperationNode
mhsdesign Apr 29, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
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
{
Expand All @@ -43,22 +45,41 @@ 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) === 2 && $unionType->containsNull()) {
$typeWithoutNull = $unionType->withoutNull();
return $this->transpileNullableType($typeWithoutNull, $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',
SlotType::class => $this->strategy->getPhpTypeReferenceForSlotType($type, $typeReferenceNode),
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 $typeWithoutNull, TypeReferenceNode $typeReferenceNode): string
{
$phpTypeWithoutNull = $this->transpileNonUnionType($typeWithoutNull, $typeReferenceNode);
return (str_contains($phpTypeWithoutNull, '|') ? 'null|' : '?') . $phpTypeWithoutNull;
}
}
72 changes: 72 additions & 0 deletions src/TypeSystem/Inferrer/InferredTypes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

/**
* PackageFactory.ComponentEngine - Universal View Components for PHP
* Copyright (C) 2022 Contributors of PackageFactory.ComponentEngine
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

declare(strict_types=1);

namespace PackageFactory\ComponentEngine\TypeSystem\Inferrer;
mhsdesign marked this conversation as resolved.
Show resolved Hide resolved

use PackageFactory\ComponentEngine\TypeSystem\TypeInterface;

class InferredTypes
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
class InferredTypes
class TypeMap

It might be sensible to move this class to a namespace PackageFactory\ComponentEngine\TypeSystem\Utility or so. It may have more uses than just this one.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still not sure ;) maybe another day ^^ It will fall all into place sometime...

{
/**
* Map of identifierName to the corresponding inferred type
* @var array<string,TypeInterface>
*/
private readonly array $types;

private function __construct(
TypeInterface ...$types
) {
assert(self::isAssociativeArray($types), '$types must be an associative array');
mhsdesign marked this conversation as resolved.
Show resolved Hide resolved
$this->types = $types;
}

public static function empty(): self
{
return new self();
}

public static function fromType(string $identifierName, TypeInterface $type): self
mhsdesign marked this conversation as resolved.
Show resolved Hide resolved
{
return new self(...[$identifierName => $type]);
}

public function getType(string $identifierName): ?TypeInterface
{
return $this->types[$identifierName] ?? null;
}

/**
* @template T
* @param array<string|int,T> $array
* @phpstan-assert-if-true array<string,T> $array
*/
private static function isAssociativeArray(array $array): bool
{
foreach ($array as $key => $value) {
if (is_string($key)) {
continue;
}
return false;
}
return true;
}
mhsdesign marked this conversation as resolved.
Show resolved Hide resolved
}
97 changes: 97 additions & 0 deletions src/TypeSystem/Inferrer/TypeInferrer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?php

/**
* PackageFactory.ComponentEngine - Universal View Components for PHP
* Copyright (C) 2022 Contributors of PackageFactory.ComponentEngine
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

declare(strict_types=1);

namespace PackageFactory\ComponentEngine\TypeSystem\Inferrer;
mhsdesign marked this conversation as resolved.
Show resolved Hide resolved

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;

/**
* 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
mhsdesign marked this conversation as resolved.
Show resolved Hide resolved
{
public function __construct(
private readonly ScopeInterface $scope
) {
}

public function inferTypesInCondition(ExpressionNode $conditionNode, TypeInferrerContext $context): InferredTypes
grebaldi marked this conversation as resolved.
Show resolved Hide resolved
{
if ($conditionNode->root instanceof IdentifierNode) {
$type = $this->scope->lookupTypeFor($conditionNode->root->value);
if (!$type) {
return InferredTypes::empty();
}
// case `nullableString ? "nullableString is not null" : "nullableString is null"`
return InferredTypes::fromType($conditionNode->root->value, $context->narrowDownType($type));
}

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 InferredTypes::empty();
}
$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 InferredTypes::empty();
}
$type = $this->scope->lookupTypeFor($comparedIdentifierValueToNull);
if (!$type) {
return InferredTypes::empty();
}

if ($binaryOperationNode->operator === BinaryOperator::EQUAL) {
return InferredTypes::fromType($comparedIdentifierValueToNull, $context->negate()->narrowDownType($type));
}
if ($binaryOperationNode->operator === BinaryOperator::NOT_EQUAL) {
return InferredTypes::fromType($comparedIdentifierValueToNull, $context->narrowDownType($type));
}
}
mhsdesign marked this conversation as resolved.
Show resolved Hide resolved

return InferredTypes::empty();
}
}
53 changes: 53 additions & 0 deletions src/TypeSystem/Inferrer/TypeInferrerContext.php
grebaldi marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

/**
* PackageFactory.ComponentEngine - Universal View Components for PHP
* Copyright (C) 2022 Contributors of PackageFactory.ComponentEngine
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

declare(strict_types=1);

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 negate(): self
{
return match ($this) {
self::TRUTHY => self::FALSY,
self::FALSY => self::TRUTHY
};
}

public function narrowDownType(TypeInterface $type): TypeInterface
{
if (!$type instanceof UnionType || !$type->containsNull()) {
return $type;
}
return match ($this) {
self::TRUTHY => $type->withoutNull(),
self::FALSY => NullType::get()
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +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\TernaryBranchScope\TernaryBranchScope;
use PackageFactory\ComponentEngine\TypeSystem\ScopeInterface;
use PackageFactory\ComponentEngine\TypeSystem\Type\UnionType\UnionType;
use PackageFactory\ComponentEngine\TypeSystem\TypeInterface;
Expand All @@ -38,20 +39,29 @@ public function __construct(

public function resolveTypeOf(TernaryOperationNode $ternaryOperationNode): TypeInterface
{
$expressionTypeResolver = new ExpressionTypeResolver(
scope: $this->scope
$trueExpressionTypeResolver = new ExpressionTypeResolver(
scope: TernaryBranchScope::forTruthyBranch(
$ternaryOperationNode->condition,
$this->scope
)
);
$conditionNode = $ternaryOperationNode->condition->root;

if ($conditionNode instanceof BooleanLiteralNode) {
return $conditionNode->value
? $expressionTypeResolver->resolveTypeOf($ternaryOperationNode->true)
: $expressionTypeResolver->resolveTypeOf($ternaryOperationNode->false);
$falseExpressionTypeResolver = new ExpressionTypeResolver(
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(
$expressionTypeResolver->resolveTypeOf($ternaryOperationNode->true),
$expressionTypeResolver->resolveTypeOf($ternaryOperationNode->false)
$trueExpressionTypeResolver->resolveTypeOf($ternaryOperationNode->true),
$falseExpressionTypeResolver->resolveTypeOf($ternaryOperationNode->false)
);
}
}
9 changes: 7 additions & 2 deletions src/TypeSystem/Scope/GlobalScope/GlobalScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
}
}
8 changes: 7 additions & 1 deletion src/TypeSystem/Scope/ModuleScope/ModuleScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand Down
Loading